diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e7b895dd..bfea554a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -65,7 +65,7 @@ jobs: uses: actions/checkout@v4 with: repository: raspberrypi/pico-sdk - ref: develop + ref: rpi-connect-preview path: pico-sdk submodules: ${{ matrix.mbedtls && 'recursive' || 'false' }} @@ -119,7 +119,7 @@ jobs: uses: actions/checkout@v4 with: repository: raspberrypi/pico-sdk - ref: develop + ref: rpi-connect-preview path: pico-sdk submodules: 'recursive' - name: Build and Install @@ -131,7 +131,7 @@ jobs: uses: actions/checkout@v4 with: repository: raspberrypi/pico-examples - ref: develop + ref: rpi-connect-preview path: pico-examples - name: Build Pico Examples run: | diff --git a/BUILD.bazel b/BUILD.bazel index 0397716f..d8b69e69 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -1,6 +1,6 @@ load("@rules_cc//cc:cc_binary.bzl", "cc_binary") load("@rules_cc//cc:cc_library.bzl", "cc_library") -load("//bazel:defs.bzl", "otp_header_parse", "picotool_binary_data_header") +load("//bazel:defs.bzl", "otp_header_parse", "picotool_binary_data_header", "provision_boards_header") package(default_visibility = ["//visibility:public"]) @@ -13,6 +13,13 @@ picotool_binary_data_header( out = "xip_ram_perms_elf.h", ) +# TODO: Make it possible to build the prebuilt from source. +picotool_binary_data_header( + name = "rpi_connect_provision_elf", + src = "//rpi_connect_provision:rpi_connect_provision_prebuilt", + out = "rpi_connect_provision_elf.h", +) + # TODO: Make it possible to build the prebuilt from source. picotool_binary_data_header( name = "enc_bootloader_elf", @@ -65,6 +72,19 @@ cc_library( ], ) +cc_library( + name = "rpi_connect_provision", + srcs = ["get_rpi_connect_provision.cpp"], + hdrs = [ + "get_rpi_connect_provision.h", + "rpi_connect_provision_elf.h", + ], + deps = [ + "//bazel:data_locs", + "//lib/whereami", + ], +) + cc_library( name = "enc_bootloader", srcs = ["get_enc_bootloader.cpp"], @@ -79,6 +99,13 @@ cc_library( ], ) +# Any board target carries all the SDK board headers +provision_boards_header( + name = "provision_boards", + board = "@pico-sdk//src/boards:pico2_w", + out = "provision_boards.h", +) + filegroup( name = "data_locs_header", srcs = ["data_locs.h"], @@ -100,10 +127,12 @@ cc_binary( "cli.h", "clipp/clipp.h", "get_enc_bootloader.cpp", + "get_rpi_connect_provision.cpp", "get_xip_ram_perms.cpp", "main.cpp", "otp.cpp", "otp.h", + "provision_boards.h", ] + select({ # MSVC can't handle long strings, so use this manually generated # header instead. @@ -139,6 +168,7 @@ cc_binary( }), deps = [ ":enc_bootloader", + ":rpi_connect_provision", ":xip_ram_perms", "//bazel:data_locs", "//bintool", diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e580bd9..72bcf283 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,8 +64,10 @@ if (NOT DEFINED USE_PRECOMPILED) endif() +# BUILD_BYPRODUCTS lists the files the project produces, so rules that depend +# on them are re-run when they change (depending on the target only orders it) function(add_embedded_data_project TARGET) - cmake_parse_arguments(PARSE_ARGV 1 OPTS "" "PREFIX;SOURCE_DIR;BINARY_DIR" "CMAKE_ARGS") + cmake_parse_arguments(PARSE_ARGV 1 OPTS "" "PREFIX;SOURCE_DIR;BINARY_DIR" "CMAKE_ARGS;BUILD_BYPRODUCTS") if (USE_PRECOMPILED) ExternalProject_Add(${TARGET} @@ -73,6 +75,7 @@ function(add_embedded_data_project TARGET) SOURCE_DIR ${OPTS_SOURCE_DIR} BINARY_DIR ${OPTS_BINARY_DIR} CMAKE_ARGS ${OPTS_CMAKE_ARGS} + BUILD_BYPRODUCTS ${OPTS_BUILD_BYPRODUCTS} INSTALL_COMMAND "" BUILD_ALWAYS 1 ) @@ -82,6 +85,7 @@ function(add_embedded_data_project TARGET) SOURCE_DIR ${OPTS_SOURCE_DIR} BINARY_DIR ${OPTS_BINARY_DIR} CMAKE_ARGS ${OPTS_CMAKE_ARGS} + BUILD_BYPRODUCTS ${OPTS_BUILD_BYPRODUCTS} BUILD_ALWAYS 1 ) endif() @@ -89,6 +93,7 @@ endfunction() # compile enc_bootloader.elf +set(ENC_BOOTLOADER_ELF ${CMAKE_BINARY_DIR}/enc_bootloader/enc_bootloader.elf) add_embedded_data_project(enc_bootloader PREFIX enc_bootloader SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/enc_bootloader @@ -99,11 +104,11 @@ add_embedded_data_project(enc_bootloader "-DUSE_PRECOMPILED:BOOL=${USE_PRECOMPILED}" "-DUSE_MBEDTLS=0" "-DPICO_DEBUG_INFO_IN_RELEASE=OFF" + BUILD_BYPRODUCTS ${ENC_BOOTLOADER_ELF} ) -set(ENC_BOOTLOADER_ELF ${CMAKE_BINARY_DIR}/enc_bootloader/enc_bootloader.elf) - if (TARGET mbedtls) + set(ENC_BOOTLOADER_MBEDTLS_ELF ${CMAKE_BINARY_DIR}/enc_bootloader_mbedtls/enc_bootloader.elf) add_embedded_data_project(enc_bootloader_mbedtls PREFIX enc_bootloader_mbedtls SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/enc_bootloader @@ -114,13 +119,13 @@ if (TARGET mbedtls) "-DUSE_PRECOMPILED:BOOL=${USE_PRECOMPILED}" "-DUSE_MBEDTLS=1" "-DPICO_DEBUG_INFO_IN_RELEASE=OFF" + BUILD_BYPRODUCTS ${ENC_BOOTLOADER_MBEDTLS_ELF} ) - - set(ENC_BOOTLOADER_MBEDTLS_ELF ${CMAKE_BINARY_DIR}/enc_bootloader_mbedtls/enc_bootloader.elf) endif() if (NOT PICOTOOL_NO_LIBUSB) # compile xip_ram_perms.elf + set(XIP_RAM_PERMS_ELF ${CMAKE_BINARY_DIR}/xip_ram_perms/xip_ram_perms.elf) add_embedded_data_project(xip_ram_perms PREFIX xip_ram_perms SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/xip_ram_perms @@ -130,11 +135,25 @@ if (NOT PICOTOOL_NO_LIBUSB) "-DPICO_SDK_PATH:FILEPATH=${PICO_SDK_PATH}" "-DUSE_PRECOMPILED:BOOL=${USE_PRECOMPILED}" "-DPICO_DEBUG_INFO_IN_RELEASE=OFF" + BUILD_BYPRODUCTS ${XIP_RAM_PERMS_ELF} ) - set(XIP_RAM_PERMS_ELF ${CMAKE_BINARY_DIR}/xip_ram_perms/xip_ram_perms.elf) + # compile rpi_connect_provision.elf + set(RPI_CONNECT_PROVISION_ELF ${CMAKE_BINARY_DIR}/rpi_connect_provision/rpi_connect_provision.elf) + add_embedded_data_project(rpi_connect_provision + PREFIX rpi_connect_provision + SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/rpi_connect_provision + BINARY_DIR ${CMAKE_BINARY_DIR}/rpi_connect_provision + CMAKE_ARGS + "-DCMAKE_MAKE_PROGRAM:FILEPATH=${CMAKE_MAKE_PROGRAM}" + "-DPICO_SDK_PATH:FILEPATH=${PICO_SDK_PATH}" + "-DUSE_PRECOMPILED:BOOL=${USE_PRECOMPILED}" + "-DPICO_DEBUG_INFO_IN_RELEASE=OFF" + BUILD_BYPRODUCTS ${RPI_CONNECT_PROVISION_ELF} + ) # compile flash_id + set(FLASH_ID_BIN ${CMAKE_BINARY_DIR}/picoboot_flash_id/flash_id.bin) add_embedded_data_project(flash_id PREFIX picoboot_flash_id SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/picoboot_flash_id @@ -144,10 +163,9 @@ if (NOT PICOTOOL_NO_LIBUSB) "-DPICO_SDK_PATH:FILEPATH=${PICO_SDK_PATH}" "-DUSE_PRECOMPILED:BOOL=${USE_PRECOMPILED}" "-DPICO_DEBUG_INFO_IN_RELEASE=OFF" + BUILD_BYPRODUCTS ${FLASH_ID_BIN} ) - set(FLASH_ID_BIN ${CMAKE_BINARY_DIR}/picoboot_flash_id/flash_id.bin) - # We want to generate headers from WELCOME.HTM etc. ExternalProject_Add(otp_header_parser PREFIX otp_header_parser @@ -171,6 +189,33 @@ if (NOT PICOTOOL_NO_LIBUSB) set_property(TARGET otp_header_parse PROPERTY IMPORTED_LOCATION ${CMAKE_BINARY_DIR}/otp_header_parser/otp_header_parse) endif() + # Board pin defaults for provision connect --board, from the SDK board headers + ExternalProject_Add(board_header_parser + PREFIX board_header_parser + SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/board_header_parser + BINARY_DIR ${CMAKE_BINARY_DIR}/board_header_parser + BUILD_ALWAYS 1 + DOWNLOAD_COMMAND "" + INSTALL_COMMAND "" + ) + + add_executable(board_header_parse IMPORTED) + add_dependencies(board_header_parse board_header_parser) + if (is_multi_config) + set_property(TARGET board_header_parse PROPERTY IMPORTED_LOCATION ${CMAKE_BINARY_DIR}/board_header_parser/${tmp_config}/board_header_parse) + else() + set_property(TARGET board_header_parse PROPERTY IMPORTED_LOCATION ${CMAKE_BINARY_DIR}/board_header_parser/board_header_parse) + endif() + + file(GLOB BOARD_HEADERS CONFIGURE_DEPENDS ${PICO_SDK_PATH}/src/boards/include/boards/*.h) + set(PROVISION_BOARDS_H ${CMAKE_CURRENT_BINARY_DIR}/provision_boards.h) + add_custom_target(generate_provision_boards DEPENDS ${PROVISION_BOARDS_H}) + add_custom_command(OUTPUT ${PROVISION_BOARDS_H} + COMMENT "Generating ${PROVISION_BOARDS_H}" + DEPENDS ${CMAKE_CURRENT_LIST_DIR}/board_header_parser/board_header_parse.cpp ${BOARD_HEADERS} + COMMAND board_header_parse ${PROVISION_BOARDS_H} ${BOARD_HEADERS} + ) + if (PICOTOOL_CODE_OTP) set(GENERATED_H ${CMAKE_CURRENT_BINARY_DIR}/otp_contents.h) add_custom_target(generate_otp_header DEPENDS ${GENERATED_H}) @@ -219,13 +264,22 @@ endif() add_custom_target(embedded_data DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/xip_ram_perms_elf.h + ${CMAKE_CURRENT_BINARY_DIR}/rpi_connect_provision_elf.h ${CMAKE_CURRENT_BINARY_DIR}/flash_id_bin.h) +add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/rpi_connect_provision_elf.h + COMMAND ${CMAKE_COMMAND} + -D BINARY_FILE=${RPI_CONNECT_PROVISION_ELF} + -D OUTPUT_NAME=rpi_connect_provision_elf + -P ${CMAKE_CURRENT_LIST_DIR}/cmake/binh.cmake + DEPENDS rpi_connect_provision ${RPI_CONNECT_PROVISION_ELF} + COMMENT "Configuring rpi_connect_provision_elf.h" + VERBATIM) add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/xip_ram_perms_elf.h COMMAND ${CMAKE_COMMAND} -D BINARY_FILE=${XIP_RAM_PERMS_ELF} -D OUTPUT_NAME=xip_ram_perms_elf -P ${CMAKE_CURRENT_LIST_DIR}/cmake/binh.cmake - DEPENDS xip_ram_perms + DEPENDS xip_ram_perms ${XIP_RAM_PERMS_ELF} COMMENT "Configuring xip_ram_perms_elf.h" VERBATIM) add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/enc_bootloader_elf.h @@ -233,7 +287,7 @@ add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/enc_bootloader_elf.h -D BINARY_FILE=${ENC_BOOTLOADER_ELF} -D OUTPUT_NAME=enc_bootloader_elf -P ${CMAKE_CURRENT_LIST_DIR}/cmake/binh.cmake - DEPENDS enc_bootloader + DEPENDS enc_bootloader ${ENC_BOOTLOADER_ELF} COMMENT "Configuring enc_bootloader_elf.h" VERBATIM) add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/enc_bootloader_mbedtls_elf.h @@ -241,7 +295,7 @@ add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/enc_bootloader_mbedtls_elf -D BINARY_FILE=${ENC_BOOTLOADER_MBEDTLS_ELF} -D OUTPUT_NAME=enc_bootloader_mbedtls_elf -P ${CMAKE_CURRENT_LIST_DIR}/cmake/binh.cmake - DEPENDS enc_bootloader_mbedtls + DEPENDS enc_bootloader_mbedtls ${ENC_BOOTLOADER_MBEDTLS_ELF} COMMENT "Configuring enc_bootloader_mbedtls_elf.h" VERBATIM) add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/flash_id_bin.h @@ -249,7 +303,7 @@ add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/flash_id_bin.h -D BINARY_FILE=${FLASH_ID_BIN} -D OUTPUT_NAME=flash_id_bin -P ${CMAKE_CURRENT_LIST_DIR}/cmake/binh.cmake - DEPENDS flash_id + DEPENDS flash_id ${FLASH_ID_BIN} COMMENT "Configuring flash_id_bin.h" VERBATIM) @@ -288,8 +342,8 @@ add_executable(picotool main.cpp) add_dependencies(picotool embedded_data_no_libusb) if (NOT PICOTOOL_NO_LIBUSB) - target_sources(picotool PRIVATE get_xip_ram_perms.cpp) - add_dependencies(picotool generate_otp_header embedded_data) + target_sources(picotool PRIVATE get_xip_ram_perms.cpp get_rpi_connect_provision.cpp) + add_dependencies(picotool generate_otp_header generate_provision_boards embedded_data) endif() set(PROJECT_VERSION 2.3.2-develop) set(PICOTOOL_VERSION 2.3.2-develop) @@ -422,4 +476,10 @@ if (NOT PICOTOOL_NO_LIBUSB) ${XIP_RAM_PERMS_ELF} DESTINATION ${INSTALL_DATADIR} ) + + #Install rpi_connect_provision.elf + install(FILES + ${RPI_CONNECT_PROVISION_ELF} + DESTINATION ${INSTALL_DATADIR} + ) endif() diff --git a/README.md b/README.md index b22ba878..758be4ba 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,9 @@ SYNOPSIS: picotool erase -r [device-selection] picotool reboot [-a] [-u] [-g ] [-c ] [device-selection] picotool seal [--quiet] [--verbose] [--hash] [--sign] [--clear] [--pin-xip-sram] - [--no-squash] [-t ] [-o ] [-t ] [] - [] [--major ] [--minor ] [--rollback [..]] + [--no-squash] [--no-ignore-others] [-t ] [-o ] + [-t ] [] [] [--major ] [--minor ] [--rollback + [..]] picotool encrypt [--quiet] [--verbose] [--embed] [--fast-rosc] [--use-mbedtls] [--otp-key-page ] [--hash] [--sign] [--no-clear] [--pin-xip-sram] [-t ] [-o ] [-t ] @@ -41,7 +42,9 @@ SYNOPSIS: picotool coprodis [--quiet] [--verbose] picotool link [--quiet] [--verbose] [-t ] [-t ] [-t ] [] [-t ] [-p ] + picotool tbyb [--quiet] [--verbose] [--clear] [-t ] [--block-num ] picotool bdev ls|mkdir|cp|rm|cat|format + picotool provision connect COMMANDS: help Show general help or help for a specific command @@ -63,7 +66,9 @@ COMMANDS: otp Commands related to the RP2350 OTP (One-Time-Programmable) Memory coprodis Post-process coprocessor instructions in disassembly files. link Link multiple binaries into one block loop. + tbyb Set TBYB bit on the best block in binary bdev Commands related to embedded block devices + provision Commands related to provisioning devices Use "picotool help " for more info ``` @@ -71,7 +76,7 @@ Use "picotool help " for more info Note commands that aren't acting on files require a device in BOOTSEL mode to be connected. ## Links to documentation for `picotool` commands -[`info`](#info) [`config`](#config) [`load`](#load) [`save`](#save) [`verify`](#verify) [`erase`](#erase) [`reboot`](#reboot) [`seal`](#seal) [`encrypt`](#encrypt) [`partition`](#partition) [`uf2`](#uf2) [`otp`](#otp) [`coprodis`](#coprodis) [`link`](#link) [`bdev`](#bdev) +[`info`](#info) [`config`](#config) [`load`](#load) [`save`](#save) [`verify`](#verify) [`erase`](#erase) [`reboot`](#reboot) [`seal`](#seal) [`encrypt`](#encrypt) [`partition`](#partition) [`uf2`](#uf2) [`otp`](#otp) [`coprodis`](#coprodis) [`link`](#link) [`tbyb`](#tbyb) [`bdev`](#bdev) [`provision`](#provision) ## Building & Installing @@ -715,8 +720,9 @@ SEAL: SYNOPSIS: picotool seal [--quiet] [--verbose] [--hash] [--sign] [--clear] [--pin-xip-sram] - [--no-squash] [-t ] [-o ] [-t ] [] - [] [--major ] [--minor ] [--rollback [..]] + [--no-squash] [--no-ignore-others] [-t ] [-o ] + [-t ] [] [] [--major ] [--minor ] [--rollback + [..]] OPTIONS: --quiet @@ -744,6 +750,8 @@ OPTIONS: Pin XIP SRAM on load --no-squash Don't squash segments in the ELF file + --no-ignore-others + Don't mark other blocks in the ELF file as ignored File to load from The file name @@ -1383,7 +1391,7 @@ OPTIONS: The file name -t - Specify file type (json | bin) explicitly, ignoring file extension + Specify file type (json | bin | pem) explicitly, ignoring file extension Target device selection --bus Filter devices by USB bus number @@ -1744,6 +1752,36 @@ OPTIONS: The file name ``` +## tbyb + +This command is used to set/clear the Try-Before-You-Buy bit in a binary. By default it will automatically pick which metadata block to operate on, but you can specify `--block-num` to operate on a specifc block (numbered from 1, matching the output of `picotool info -m`). + +```text +$ picotool help tbyb +TBYB: + Set TBYB bit on the best block in binary + +SYNOPSIS: + picotool tbyb [--quiet] [--verbose] [--clear] [-t ] [--block-num ] + +OPTIONS: + --quiet + Don't print any output + --verbose + Print verbose output + --clear + Clear the TBYB bit instead + --block-num + Explicitly specify which block to modify, indexed from 1 (matching the `picotol + info -m` output) + File to modify + + The file name + -t + Specify file type (uf2 | elf | bin) explicitly, ignoring file extension +``` + + ## bdev The `bdev` commands are for interacting with block devices in Flash. The block device location can either be determined using binary info, or you can specify a partition to use as a block device. The commands work with a LittleFS filesystem, or a FatFS filesystem. @@ -2211,6 +2249,130 @@ OPTIONS: The BOOTSEL activity LED is active low (ignored by RP2040 and RP2350-A4) ``` +## provision + +Support for running provisioning programs in SRAM on devices + +### connect + +Provision a device for use with Raspberry Pi Connect. + +```text +$ picotool help provision connect +PROVISION CONNECT: + Provision the device for Raspberry Pi Connect, by running a provisioning binary on it from + RAM. Secrets are only written into the binary in RAM, never to flash. When done the device + flashes its LED (slowly on success, quickly on failure) and reboots to BOOTSEL; output is + on its USB and UART consoles. The device must have a partition table with an FFS partition, + and a WiFi firmware partition containing the firmware + +SYNOPSIS: + picotool provision connect [--create-identity] [--identity-exchange] [--auth-key ] + [--signin] [--token ] [--clear] [--wifi-only] [--org-token ] + [--description ] [--device-name ] [--client-id ] [--wifi-ssid + ] [--wifi-password ] [--board ] [--uart ] + [--uart-tx ] [--uart-rx ] [--uart-baud ] [--led ] + [--wl-reg-on ] [--wl-data-out ] [--wl-data-in ] [--wl-host-wake + ] [--wl-clock ] [--wl-cs ] [--hash] [--sign] [] + [device-selection] + +OPTIONS: + Operation (exactly one) + --create-identity + Register the device's OTP identity key with an organisation (requires --org-token), + and clear any stored access token so the new identity is used + --identity-exchange + Exchange the registered OTP identity for an access token, and store it on the + device + --auth-key + Exchange a provisioning auth key for an access token, and store it on the device + --signin + Sign in with a code shown on the device's console, and store the access token on + the device + --token + Store the given access token on the device + --clear + Clear the stored access token and any deployment state + --wifi-only + Only store the WiFi credentials (requires --wifi-ssid) + Operation Options + --org-token + Organisation token, for --create-identity + --description + Description of the device identity, for --create-identity + --device-name + Device name (default pico-) + --client-id + Raspberry Pi Connect client ID (default the SDK's) + WiFi Credentials + --wifi-ssid + WiFi network to store on the device before the operation (requires --wifi-password) + --wifi-password + WiFi password + Board Configuration (defaults are for a Pico 2 W, unless --board is given) + --board + SDK board to use the default pins from + --uart + UART for console output, or -1 for none + --uart-tx + UART TX pin + --uart-rx + UART RX pin + --uart-baud + UART baud rate + --led + LED pin to flash when done, or -1 for the wireless chip's LED + --wl-reg-on + Wireless chip power pin + --wl-data-out + Wireless chip SPI data out pin + --wl-data-in + Wireless chip SPI data in pin + --wl-host-wake + Wireless chip host wake pin + --wl-clock + Wireless chip SPI clock pin + --wl-cs + Wireless chip SPI chip select pin + Signing Configuration + --hash + Hash the executable + --sign + Sign the executable + + Key file (.pem) + Target device selection + --bus + Filter devices by USB bus number + --address + Filter devices by USB device address + --vid + Filter by vendor id + --pid + Filter by product id + --ser + Filter by serial number + --rp2040 + Assume the device is an RP2040 - this is only required when using a custom vid/pid + with an RP2040 on Windows, and is ignored on other operating systems + -f, --force + Force a device not in BOOTSEL mode but running compatible code to reset so the + command can be executed. After executing the command (unless the command itself is + a 'reboot') the device will be rebooted back to application mode + -F, --force-no-reboot + Force a device not in BOOTSEL mode but running compatible code to reset so the + command can be executed. After executing the command (unless the command itself is + a 'reboot') the device will be left connected and accessible to picotool, but + without the USB drive mounted + --bootsel-led + Specify the GPIO for the BOOTSEL activity LED to flash (default none, ignored by + RP2350A-A2 in Arm mode) - only applicable if this command reboots the device to + BOOTSEL mode + --bootsel-led-active-low + The BOOTSEL activity LED is active low (ignored by RP2040 and RP2350-A4) +``` + + ## Binary Information Binary information is machine locatable and generally machine consumable. I say generally because anyone can diff --git a/bazel/defs.bzl b/bazel/defs.bzl index d4d10b64..04cf89b1 100644 --- a/bazel/defs.bzl +++ b/bazel/defs.bzl @@ -1,4 +1,5 @@ load("@bazel_skylib//rules:run_binary.bzl", "run_binary") +load("@rules_cc//cc/common:cc_info.bzl", "CcInfo") def picotool_binary_data_header(name, src, out, **kwargs): run_binary( @@ -38,3 +39,37 @@ def otp_header_parse(name, src, out, **kwargs): tool = "@picotool//bazel:jsonh", **kwargs ) + +def _provision_boards_header_impl(ctx): + # A board's cc_library carries all the board headers (plus a few others) + headers = [ + f + for f in ctx.attr.board[CcInfo].compilation_context.headers.to_list() + if "include/boards/" in f.path and f.basename.endswith(".h") + ] + args = ctx.actions.args() + args.add(ctx.outputs.out) + args.add_all(headers) + ctx.actions.run( + executable = ctx.executable._tool, + arguments = [args], + inputs = headers, + outputs = [ctx.outputs.out], + mnemonic = "ProvisionBoards", + progress_message = "Generating %{output}", + ) + return [DefaultInfo(files = depset([ctx.outputs.out]))] + +# Generates the provision connect --board pin table from the SDK board headers +provision_boards_header = rule( + implementation = _provision_boards_header_impl, + attrs = { + "board": attr.label(providers = [CcInfo]), + "out": attr.output(mandatory = True), + "_tool": attr.label( + default = "@picotool//board_header_parser:board_header_parser", + executable = True, + cfg = "exec", + ), + }, +) diff --git a/board_header_parser/BUILD.bazel b/board_header_parser/BUILD.bazel new file mode 100644 index 00000000..d6acd1f1 --- /dev/null +++ b/board_header_parser/BUILD.bazel @@ -0,0 +1,8 @@ +load("@rules_cc//cc:cc_binary.bzl", "cc_binary") + +package(default_visibility = ["//visibility:public"]) + +cc_binary( + name = "board_header_parser", + srcs = ["board_header_parse.cpp"], +) diff --git a/board_header_parser/CMakeLists.txt b/board_header_parser/CMakeLists.txt new file mode 100644 index 00000000..2aa10ac8 --- /dev/null +++ b/board_header_parser/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.12) +PROJECT(board_header_parser CXX) +set(CMAKE_CXX_STANDARD 14) + +add_executable(board_header_parse board_header_parse.cpp) diff --git a/board_header_parser/board_header_parse.cpp b/board_header_parser/board_header_parse.cpp new file mode 100644 index 00000000..7f541db5 --- /dev/null +++ b/board_header_parser/board_header_parse.cpp @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2025 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +// Generates the table of board pin defaults used by `picotool provision connect --board`, +// from the Pico SDK board headers. +// +// Usage: board_header_parse ... +// +// Only RP2350 boards with a CYW43 wireless chip are included, as the provisioning binary +// needs WiFi. The headers are read as text, following #include "boards/..." like the SDK's +// generic_board.cmake does: the first #define of a macro wins (as the #ifndef guards make +// it), unless it is #undef'd first. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using std::string; + +struct board_state { + std::map defines; + std::map cmake_sets; + std::set included; +}; + +static string basename_of(const string &path) { + size_t slash = path.find_last_of("/\\"); + return slash == string::npos ? path : path.substr(slash + 1); +} + +static bool parse_file(const string &path, const std::map &headers, board_state &state) { + if (!state.included.insert(path).second) return true; + std::ifstream in(path); + if (!in) { + std::cerr << "Cannot open " << path << "\n"; + return false; + } + static const std::regex define_re(R"re(^\s*#\s*define\s+([A-Za-z_][A-Za-z0-9_]*)\s+(.*?)\s*(//.*)?$)re"); + static const std::regex undef_re(R"re(^\s*#\s*undef\s+([A-Za-z_][A-Za-z0-9_]*))re"); + static const std::regex include_re(R"re(^\s*#\s*include\s*"boards/([^"]+)")re"); + static const std::regex cmake_set_re(R"re(^\s*pico_board_cmake_set(_default)?\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*,\s*(.*?)\s*\))re"); + string line; + std::smatch m; + while (std::getline(in, line)) { + if (std::regex_search(line, m, define_re)) { + // first definition wins, as with the #ifndef guards + state.defines.emplace(m[1].str(), m[2].str()); + } else if (std::regex_search(line, m, undef_re)) { + state.defines.erase(m[1].str()); + } else if (std::regex_search(line, m, include_re)) { + auto it = headers.find(m[1].str()); + if (it != headers.end() && !parse_file(it->second, headers, state)) return false; + } else if (std::regex_search(line, m, cmake_set_re)) { + // pico_board_cmake_set always sets, pico_board_cmake_set_default only if unset + if (m[1].matched) { + state.cmake_sets.emplace(m[2].str(), m[3].str()); + } else { + state.cmake_sets[m[2].str()] = m[3].str(); + } + } + } + return true; +} + +// Returns the integer value of a macro, following macros defined as other macros +static bool get_int(const board_state &state, const string &name, long &value, int depth = 0) { + auto it = state.defines.find(name); + if (it == state.defines.end() || depth > 8) return false; + string v = it->second; + // strip any surrounding brackets and unsigned/long suffixes + while (v.size() >= 2 && v.front() == '(' && v.back() == ')') v = v.substr(1, v.size() - 2); + while (!v.empty() && strchr("uUlL", v.back())) v.pop_back(); + try { + size_t pos; + value = std::stol(v, &pos, 0); + if (pos == v.size()) return true; + } catch (std::exception &) { + } + return get_int(state, v, value, depth + 1); +} + +int main(int argc, char **argv) { + if (argc < 2) { + std::cerr << "Usage: " << argv[0] << " ...\n"; + return 1; + } + // boards/.h -> path, for resolving includes + std::map headers; + for (int i = 2; i < argc; i++) { + headers[basename_of(argv[i])] = argv[i]; + } + + std::vector entries; + for (auto &h : headers) { + board_state state; + if (!parse_file(h.second, headers, state)) return 1; + auto platform = state.cmake_sets.find("PICO_PLATFORM"); + auto cyw43 = state.cmake_sets.find("PICO_CYW43_SUPPORTED"); + if (platform == state.cmake_sets.end() || platform->second.rfind("rp2350", 0) != 0) continue; + if (cyw43 == state.cmake_sets.end() || cyw43->second != "1") continue; + + auto field = [&](const char *macro) { + long v; + return get_int(state, macro, v) ? std::to_string(v) : string("provision_unset"); + }; + string uart, uart_tx, uart_rx; + long v; + if (get_int(state, "PICO_DEFAULT_UART", v)) { + uart = std::to_string(v); + uart_tx = get_int(state, "PICO_DEFAULT_UART_TX_PIN", v) ? std::to_string(v) : "-1"; + uart_rx = get_int(state, "PICO_DEFAULT_UART_RX_PIN", v) ? std::to_string(v) : "-1"; + } else { + // no UART on this board + uart = "-1"; + uart_tx = uart_rx = "provision_unset"; + } + // a GPIO LED if there is one, otherwise the wireless chip's + string led = get_int(state, "PICO_DEFAULT_LED_PIN", v) ? std::to_string(v) : "-1"; + + string name = h.first.substr(0, h.first.size() - 2); + entries.push_back(" {\"" + name + "\", " + uart + ", " + uart_tx + ", " + uart_rx + ", " + + field("PICO_DEFAULT_UART_BAUD_RATE") + ", " + led + ", " + + field("CYW43_DEFAULT_PIN_WL_REG_ON") + ", " + + field("CYW43_DEFAULT_PIN_WL_DATA_OUT") + ", " + + field("CYW43_DEFAULT_PIN_WL_DATA_IN") + ", " + + field("CYW43_DEFAULT_PIN_WL_HOST_WAKE") + ", " + + field("CYW43_DEFAULT_PIN_WL_CLOCK") + ", " + + field("CYW43_DEFAULT_PIN_WL_CS") + "},"); + } + + std::ofstream out(argv[1]); + if (!out) { + std::cerr << "Cannot write " << argv[1] << "\n"; + return 1; + } + out << "// Generated by board_header_parse from the Pico SDK board headers - do not edit\n"; + out << "// {name, uart, uart_tx, uart_rx, uart_baud, led, wl_reg_on, wl_data_out, wl_data_in, wl_host_wake, wl_clock, wl_cs}\n"; + for (auto &e : entries) out << e << "\n"; + return 0; +} diff --git a/cmake/binh.cmake b/cmake/binh.cmake index 6a80f49b..455484e3 100644 --- a/cmake/binh.cmake +++ b/cmake/binh.cmake @@ -2,24 +2,14 @@ file(READ ${BINARY_FILE} FILE_CONTENT HEX) string(LENGTH ${FILE_CONTENT} FILE_CONTENT_LENGTH) math(EXPR BIN_LENGTH "${FILE_CONTENT_LENGTH} / 2") -math(EXPR offset "0") - -while(FILE_CONTENT_LENGTH GREATER 0) - - if(FILE_CONTENT_LENGTH GREATER 32) - math(EXPR length "32") - else() - math(EXPR length "${FILE_CONTENT_LENGTH}") - endif() - - string(SUBSTRING ${FILE_CONTENT} ${offset} ${length} line) - set(lines "${lines}\n${line}") - - math(EXPR FILE_CONTENT_LENGTH "${FILE_CONTENT_LENGTH} - ${length}") - math(EXPR offset "${offset} + ${length}") -endwhile() - -set(FILE_CONTENT "${lines}") +# split into lines of 16 bytes (32 hex characters), each preceded by a newline +math(EXPR full_length "${FILE_CONTENT_LENGTH} - ${FILE_CONTENT_LENGTH} % 32") +string(SUBSTRING "${FILE_CONTENT}" 0 ${full_length} full_lines) +string(SUBSTRING "${FILE_CONTENT}" ${full_length} -1 last_line) +string(REGEX REPLACE "(................................)" "\n\\1" FILE_CONTENT "${full_lines}") +if (NOT last_line STREQUAL "") + set(FILE_CONTENT "${FILE_CONTENT}\n${last_line}") +endif() # adds '0x' prefix and comma suffix before and after every byte respectively string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1, " FILE_CONTENT ${FILE_CONTENT}) diff --git a/get_rpi_connect_provision.cpp b/get_rpi_connect_provision.cpp new file mode 100644 index 00000000..da9d171b --- /dev/null +++ b/get_rpi_connect_provision.cpp @@ -0,0 +1,39 @@ + +#include +#include +#include +#include +#include + +#include "get_rpi_connect_provision.h" +#include "rpi_connect_provision_elf.h" + +#include "data_locs.h" + +#include "whereami++.h" + + +std::shared_ptr get_rpi_connect_provision() { + // search same directory as executable + whereami::whereami_path_t executablePath = whereami::getExecutablePath(); + std::string local_loc = executablePath.dirname() + "/"; + if (std::find(data_locs.begin(), data_locs.end(), local_loc) == data_locs.end()) { + data_locs.insert(data_locs.begin(), local_loc); + } + + for (auto loc : data_locs) { + std::string filename = loc + "rpi_connect_provision.elf"; + std::ifstream i(filename); + if (i.good()) { + printf("Picking file %s\n", filename.c_str()); + auto file = std::make_shared(filename, std::ios::in|std::ios::binary); + return file; + } + } + + // fall back to embedded rpi_connect_provision.elf file + printf("Could not find rpi_connect_provision.elf file - using embedded binary\n"); + auto tmp = std::make_shared(); + tmp->write(reinterpret_cast(rpi_connect_provision_elf), rpi_connect_provision_elf_SIZE); + return tmp; +} diff --git a/get_rpi_connect_provision.h b/get_rpi_connect_provision.h new file mode 100644 index 00000000..916b475f --- /dev/null +++ b/get_rpi_connect_provision.h @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2025 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#pragma once + +#include +#include + +std::shared_ptr get_rpi_connect_provision(); diff --git a/main.cpp b/main.cpp index 38c467b3..50fe05e9 100644 --- a/main.cpp +++ b/main.cpp @@ -38,6 +38,7 @@ #if HAS_LIBUSB #include "picoboot_connection_cxx.h" #include "get_xip_ram_perms.h" + #include "get_rpi_connect_provision.h" #include "lfs.h" #include "ff.h" #include "diskio.h" @@ -105,6 +106,12 @@ static __forceinline int __builtin_ctz(unsigned x) { #ifndef BLOCK_DEVICE_DEFAULT_PARTITION_ID #define BLOCK_DEVICE_DEFAULT_PARTITION_ID 0x626C6F636B646576 #endif +#ifndef FFS_DATA_PARTITION_ID +#define FFS_DATA_PARTITION_ID 0x746d656673665f6f +#endif +#ifndef CYW43_FIRMWARE_PARTITION_ID +#define CYW43_FIRMWARE_PARTITION_ID 0x776966696669726d +#endif // ------ using std::string; @@ -550,6 +557,9 @@ struct multi_cmd : public cmd { #define DEFAULT_BOOTSEL_LED -1 #endif +// provision integer settings left at this are not configured, keeping the binary's defaults +constexpr int provision_unset = std::numeric_limits::min(); + struct _settings { std::array filenames; std::array file_types; @@ -633,6 +643,7 @@ struct _settings { bool pin_xip_sram = false; bool set_tbyb = false; bool no_squash = false; + bool ignore_others = true; uint16_t major_version = 0; uint16_t minor_version = 0; uint16_t rollback_version = 0; @@ -652,6 +663,11 @@ struct _settings { uint32_t align = 0x1000; } link; + struct { + bool clear = false; + int block_num = -1; + } tbyb; + struct { bool all = false; bool verify = false; @@ -697,6 +713,34 @@ struct _settings { bool force_formattable = false; bool force_writeable = false; } bdev; + + struct { + string board; + bool create_identity = false; + bool identity_exchange = false; + bool signin = false; + bool clear = false; + bool wifi_only = false; + string auth_key; + string token; + string org_token; + string description; + string device_name; + string client_id; + string wifi_ssid; + string wifi_password; + int uart = provision_unset; + int uart_tx = provision_unset; + int uart_rx = provision_unset; + int uart_baud = provision_unset; + int led = provision_unset; + int wl_reg_on = provision_unset; + int wl_data_out = provision_unset; + int wl_data_in = provision_unset; + int wl_host_wake = provision_unset; + int wl_clock = provision_unset; + int wl_cs = provision_unset; + } provision; }; _settings settings; std::shared_ptr selected_cmd; @@ -1203,7 +1247,8 @@ struct seal_command : public cmd { option("--sign").set(settings.seal.sign) % "Sign the file" + option("--clear").set(settings.seal.clear_sram) % "Clear all of main SRAM on load" + option("--pin-xip-sram").set(settings.seal.pin_xip_sram) % "Pin XIP SRAM on load" + - option("--no-squash").set(settings.seal.no_squash) % "Don't squash segments in the ELF file" + option("--no-squash").set(settings.seal.no_squash) % "Don't squash segments in the ELF file" + + option("--no-ignore-others").clear(settings.seal.ignore_others) % "Don't mark other blocks in the ELF file as ignored" ).min(0).doc_non_optional(true) % "Configuration" + named_file_selection_x("infile", 0) % "File to load from" + ( @@ -1257,6 +1302,26 @@ struct link_command : public cmd { } }; +struct tbyb_command : public cmd { + tbyb_command() : cmd("tbyb") {} + bool execute(device_map &devices) override; + virtual device_support get_device_support() override { return none; } + + group get_cli() override { + return ( + option("--quiet").set(settings.quiet) % "Don't print any output" + + option("--verbose").set(settings.verbose) % "Print verbose output" + + option("--clear").set(settings.tbyb.clear) % "Clear the TBYB bit instead" + + named_file_selection_x("file", 0) % "File to modify" + + (option("--block-num") & integer("index").set(settings.tbyb.block_num).min(1)) % "Explicitly specify which block to modify, indexed from 1 (matching the `picotol info -m` output)" + ); + } + + string get_doc() const override { + return "Set TBYB bit on the best block in binary"; + } +}; + #if HAS_LIBUSB struct partition_info_command : public cmd { partition_info_command() : cmd("info") {} @@ -1451,7 +1516,7 @@ struct otp_load_command : public cmd { (option('s', "--start_row") & integer("row").set(settings.otp.row)) % "Start row to load at (note use 0x for hex)" + (option('i', "--include") & value("filename").add_to(settings.otp.extra_files)).min(0).max(1) % "Include extra otp definition" // todo more than 1 ).min(0).doc_non_optional(true) % "Row options" + - named_typed_file_selection_x("filename", 0, "json | bin") % "File to load row(s) from" + + named_typed_file_selection_x("filename", 0, "json | bin | pem") % "File to load row(s) from" + device_selection % "Target device selection" ); } @@ -1567,6 +1632,78 @@ struct otp_command : public multi_cmd { } }; +#if HAS_LIBUSB +struct provision_connect_command : public cmd { + provision_connect_command() : cmd("connect") {} + virtual bool requires_rp2350() const override { return true; } + + bool execute(device_map& devices) override; + + group get_cli() override { + auto &p = settings.provision; + return ( + ( + option("--create-identity").set(p.create_identity) % "Register the device's OTP identity key with an organisation (requires --org-token), and clear any stored access token so the new identity is used" + + option("--identity-exchange").set(p.identity_exchange) % "Exchange the registered OTP identity for an access token, and store it on the device" + + (option("--auth-key") & value("key").set(p.auth_key)) % "Exchange a provisioning auth key for an access token, and store it on the device" + + option("--signin").set(p.signin) % "Sign in with a code shown on the device's console, and store the access token on the device" + + (option("--token") & value("token").set(p.token)) % "Store the given access token on the device" + + option("--clear").set(p.clear) % "Clear the stored access token and any deployment state" + + option("--wifi-only").set(p.wifi_only) % "Only store the WiFi credentials (requires --wifi-ssid)" + ).min(0).doc_non_optional(true) % "Operation (exactly one)" + + ( + (option("--org-token") & value("token").set(p.org_token)) % "Organisation token, for --create-identity" + + (option("--description") & value("text").set(p.description)) % "Description of the device identity, for --create-identity" + + (option("--device-name") & value("name").set(p.device_name)) % "Device name (default pico-)" + + (option("--client-id") & value("uuid").set(p.client_id)) % "Raspberry Pi Connect client ID (default the SDK's)" + ).min(0).doc_non_optional(true) % "Operation Options" + + ( + (option("--wifi-ssid") & value("ssid").set(p.wifi_ssid)) % "WiFi network to store on the device before the operation (requires --wifi-password)" + + (option("--wifi-password") & value("password").set(p.wifi_password)) % "WiFi password" + ).min(0).doc_non_optional(true) % "WiFi Credentials" + + ( + (option("--board") & value("name").set(p.board)) % "SDK board to use the default pins from" + + (option("--uart") & integer("uart").min_value(-1).max_value(1).set(p.uart)) % "UART for console output, or -1 for none" + + (option("--uart-tx") & integer("pin").min_value(-1).max_value(47).set(p.uart_tx)) % "UART TX pin" + + (option("--uart-rx") & integer("pin").min_value(-1).max_value(47).set(p.uart_rx)) % "UART RX pin" + + (option("--uart-baud") & integer("baud").min_value(1).set(p.uart_baud)) % "UART baud rate" + + (option("--led") & integer("pin").min_value(-1).max_value(47).set(p.led)) % "LED pin to flash when done, or -1 for the wireless chip's LED" + + (option("--wl-reg-on") & integer("pin").min_value(0).max_value(47).set(p.wl_reg_on)) % "Wireless chip power pin" + + (option("--wl-data-out") & integer("pin").min_value(0).max_value(47).set(p.wl_data_out)) % "Wireless chip SPI data out pin" + + (option("--wl-data-in") & integer("pin").min_value(0).max_value(47).set(p.wl_data_in)) % "Wireless chip SPI data in pin" + + (option("--wl-host-wake") & integer("pin").min_value(0).max_value(47).set(p.wl_host_wake)) % "Wireless chip host wake pin" + + (option("--wl-clock") & integer("pin").min_value(0).max_value(47).set(p.wl_clock)) % "Wireless chip SPI clock pin" + + (option("--wl-cs") & integer("pin").min_value(0).max_value(47).set(p.wl_cs)) % "Wireless chip SPI chip select pin" + ).min(0).doc_non_optional(true) % "Board Configuration (defaults are for a Pico 2 W, unless --board is given)" + + ( + option("--hash").set(settings.seal.hash) % "Hash the executable" + + option("--sign").set(settings.seal.sign) % "Sign the executable" + + optional_untyped_file_selection_x("key", 0) % "Key file (.pem)" + ).min(0).doc_non_optional(true) % "Signing Configuration" + + device_selection % "Target device selection" + ); + } + + string get_doc() const override { + return "Provision the device for Raspberry Pi Connect, by running a provisioning binary on it from RAM. " + "Secrets are only written into the binary in RAM, never to flash. When done the device flashes its LED " + "(slowly on success, quickly on failure) and reboots to BOOTSEL; output is on its USB and UART consoles. " + "The device must have a partition table with an FFS partition, and a WiFi firmware partition containing the firmware"; + } +}; + +vector> provision_sub_commands { + std::shared_ptr(new provision_connect_command()), +}; + +struct provision_command : public multi_cmd { + provision_command() : multi_cmd("provision", provision_sub_commands) {} + string get_doc() const override { + return "Commands related to provisioning devices"; + } +}; +#endif + #if HAS_LIBUSB struct uf2_info_command : public cmd { uf2_info_command() : cmd("info") {} @@ -1812,9 +1949,13 @@ vector> commands { std::shared_ptr(new otp_command()), std::shared_ptr(new coprodis_command()), std::shared_ptr(new link_command()), + std::shared_ptr(new tbyb_command()), #if HAS_LIBUSB std::shared_ptr(new bdev_command()), #endif + #if HAS_LIBUSB + std::shared_ptr(new provision_command()), + #endif }; template @@ -3566,7 +3707,9 @@ std::vector> find_all_blocks(memory_access &raw_access, v DEBUG_LOG("Now reading from %x size %x\n", offset, size); bin = raw_access.read_vector(offset, size, true); }; - return get_all_blocks(bin, raw_access.get_binary_start(), first_block, more_cb); + auto all_blocks = get_all_blocks(bin, raw_access.get_binary_start(), first_block, more_cb); + all_blocks.insert(all_blocks.begin(), std::move(first_block)); + return all_blocks; } return std::vector>(); @@ -4769,6 +4912,46 @@ bool info_command::execute(device_map &devices) { fos << "File " << settings.filenames[0] << ":\n\n"; } info_guts(access, nullptr); + + vector bin; + vector starts; + bool partition_in_file = false; + auto blocks = find_all_blocks(access, bin); + for (auto &block : blocks) { + auto partition_table = block->get_item(); + if (partition_table == nullptr) { + continue; + } + + for (auto partition : partition_table->partitions) { + starts.push_back(partition.first_sector * 4096); + // check if partition contents is in file, if not set start to -1 + try { + uint8_t buffer[32] = {}; + access.read(access.get_model()->flash_start() + starts.back(), buffer, sizeof(buffer), false); + partition_in_file = true; + } catch (not_mapped_exception &) { + starts.back() = -1; + } + } + break; + } + + if (partition_in_file) { // at least one partition is in the file + for (unsigned int i=0; i < starts.size(); i++) { + int32_t start = starts[i]; + if (start > 0) { + fos.first_column(0); fos.hanging_indent(0); + fos << "\nPartition " << i << "\n"; + fos.first_column(1); + partition_memory_access part_access(access, (uint32_t)start); + info_guts(part_access, nullptr); + } else { + fos.first_column(0); fos.hanging_indent(0); + fos << "\nPartition " << i << " not present in file\n"; + } + } + } } return false; } @@ -5500,7 +5683,7 @@ void sign_guts_elf(elf_file* elf, private_t private_key, public_t public_key, mo } // Workaround RP2350-E13, which means when using rollback versions, all other blocks must be set as ignored - block new_block = place_new_block(elf, first_block, model, settings.seal.rollback_version); + block new_block = place_new_block(elf, first_block, model, settings.seal.rollback_version || settings.seal.ignore_others); if (settings.seal.set_tbyb) { // Set the TBYB bit on the image_type_item @@ -5593,7 +5776,7 @@ vector sign_guts_bin(iostream_memory_access in, private_t private_key, } // Workaround RP2350-E13, which means when using rollback versions, all other blocks must be set as ignored - block new_block = place_new_block(bin, bin_start, first_block, model, settings.seal.rollback_version); + block new_block = place_new_block(bin, bin_start, first_block, model, settings.seal.rollback_version || settings.seal.ignore_others); if (settings.seal.major_version || settings.seal.minor_version || settings.seal.rollback_version) { std::shared_ptr version = new_block.get_item(); @@ -6284,6 +6467,45 @@ bool seal_command::execute(device_map &devices) { } #endif +bool tbyb_command::execute(device_map &devices) { + auto access = get_file_memory_access(0, true); + + vector bin; + std::unique_ptr selected_block; + if (settings.tbyb.block_num >= 0) { + auto blocks = find_all_blocks(access, bin); + auto num_blocks = blocks.size(); + if (settings.tbyb.block_num > num_blocks) { + fail(ERROR_ARGS, "Only %d blocks in the binary, but block number %d was specified\n", num_blocks, settings.tbyb.block_num); + } + selected_block = std::move(blocks[settings.tbyb.block_num - 1]); + } else { + selected_block = find_best_block(access, bin); + } + + if (selected_block) { + DEBUG_LOG("Checking block at %x\n", selected_block->physical_addr); + // Image Def + auto image_def = selected_block->get_item(); + if (image_def != nullptr) { + DEBUG_LOG("Image def found with TBYB %d\n", image_def->tbyb()); + if (settings.tbyb.clear) { + image_def->flags &= ~PICOBIN_IMAGE_TYPE_EXE_TBYB_BITS; + } else { + image_def->flags |= PICOBIN_IMAGE_TYPE_EXE_TBYB_BITS; + } + assert(image_def->tbyb()); + std::vector words = selected_block->to_words(); + access.write_vector(selected_block->physical_addr, words); + fos << "set TBYB bit for block at " << hex_string(selected_block->physical_addr) << " to " << image_def->tbyb() << "\n"; + } else if (settings.tbyb.block_num >= 0) { + fail(ERROR_ARGS, "No image def found in block number %d (address %08x)", settings.tbyb.block_num, selected_block->physical_addr); + } + } + + return false; +} + bool link_command::execute(device_map &devices) { if (get_file_type() != filetype::bin) { fail(ERROR_ARGS, "Can only link to BINs"); @@ -9447,7 +9669,7 @@ bool otp_load_command::execute(device_map &devices) { auto model = raw_access.get_model(); // todo pre-check page lock struct picoboot_otp_cmd otp_cmd; - std::shared_ptr file = get_file(ios::in|ios::binary); + std::shared_ptr file = get_file(ios::in|ios::binary); if (get_file_type() == filetype::json) { hack_init_otp_regs(); json otp_json = json::parse(*file); @@ -9468,6 +9690,26 @@ bool otp_load_command::execute(device_map &devices) { // Return now, don't do rest of function return false; } + + // Write PEM key as ECC data + if (get_file_type() == filetype::pem) { + #if HAS_MBEDTLS + settings.otp.ecc = true; + if (settings.otp.raw) fail(ERROR_ARGS, "Cannot write PEM file with --raw"); + + private_t private_key = {}; + public_t public_key = {}; + read_keys(settings.filenames[0], &public_key, &private_key); + + auto key_bytes = std::make_shared(); + key_bytes->write((char*)private_key.bytes, sizeof(private_key.bytes)); + + file = key_bytes; + #else + fail(ERROR_ARGS, "Cannot read PEM keys with no mbedtls\n"); + #endif + } + otp_cmd.wRow = settings.otp.row; otp_cmd.bEcc = settings_select_ecc(); unsigned int row_size = otp_cmd.bEcc ? 2 : 4; @@ -9837,6 +10079,199 @@ bool otp_permissions_command::execute(device_map &devices) { return true; } +// Pin defaults of the SDK's RP2350 boards with a wireless chip, generated from their board headers +struct provision_board { + const char *name; + int uart, uart_tx, uart_rx, uart_baud, led; + int wl_reg_on, wl_data_out, wl_data_in, wl_host_wake, wl_clock, wl_cs; +}; +static const provision_board provision_boards[] = { +#include "provision_boards.h" +}; + +bool provision_connect_command::execute(device_map &devices) { + auto &p = settings.provision; + + if (!p.board.empty()) { + auto board = std::find_if(std::begin(provision_boards), std::end(provision_boards), + [&](const provision_board &b) { return p.board == b.name; }); + if (board == std::end(provision_boards)) { + string names; + for (auto &b : provision_boards) names += string("\n ") + b.name; + fail(ERROR_ARGS, "Unknown board '%s' - the SDK boards with a wireless chip are:%s", p.board.c_str(), names.c_str()); + } + // Options given explicitly override the board's pins + for (auto &i : std::vector>{ + {&p.uart, board->uart}, {&p.uart_tx, board->uart_tx}, {&p.uart_rx, board->uart_rx}, + {&p.uart_baud, board->uart_baud}, {&p.led, board->led}, + {&p.wl_reg_on, board->wl_reg_on}, {&p.wl_data_out, board->wl_data_out}, + {&p.wl_data_in, board->wl_data_in}, {&p.wl_host_wake, board->wl_host_wake}, + {&p.wl_clock, board->wl_clock}, {&p.wl_cs, board->wl_cs}}) { + if (*i.first == provision_unset) *i.first = i.second; + } + } + + const char *operation = nullptr; + int num_operations = 0; + for (auto &op : std::vector>{ + {p.create_identity, "create_identity"}, + {p.identity_exchange, "identity_exchange"}, + {!p.auth_key.empty(), "auth_key"}, + {p.signin, "signin"}, + {!p.token.empty(), "store_token"}, + {p.clear, "clear"}, + {p.wifi_only, "wifi"}}) { + if (op.first) { + operation = op.second; + num_operations++; + } + } + if (num_operations != 1) { + fail(ERROR_ARGS, "Exactly one of --create-identity, --identity-exchange, --auth-key, --signin, --token, --clear or --wifi-only must be specified"); + } + if (p.create_identity && p.org_token.empty()) { + fail(ERROR_ARGS, "--create-identity requires --org-token"); + } + if (p.wifi_ssid.empty() != p.wifi_password.empty()) { + fail(ERROR_ARGS, "--wifi-ssid and --wifi-password must be specified together"); + } + if (p.wifi_only && p.wifi_ssid.empty()) { + fail(ERROR_ARGS, "--wifi-only requires --wifi-ssid"); + } + +#if HAS_MBEDTLS + if (settings.seal.sign && settings.filenames[0].empty()) { + fail(ERROR_ARGS, "missing key file for signing"); + } + if (!settings.filenames[0].empty() && get_file_type_idx(0) != filetype::pem) { + fail(ERROR_ARGS, "Can only read pem keys"); + } +#else + if (settings.seal.sign) fail(ERROR_NOT_POSSIBLE, "Cannot sign binaries without mbedtls"); +#endif + + auto con = get_single_picoboot_cmd_compatible_device_connection("provision connect", devices, {PC_REBOOT2}); + + // The binary runs from RAM, but keeps its state in the FFS partition, and + // loads the wireless chip's firmware from its partition(s) + // The wireless chip is only started for the network operations, or to flash its LED + bool uses_wireless = p.create_identity || p.identity_exchange || !p.auth_key.empty() || p.signin || + p.led == provision_unset || p.led < 0; + auto partitions = get_partitions(con); + if (!partitions) { + fail(ERROR_NOT_POSSIBLE, "The device has no partition table - it needs an FFS partition%s", + uses_wireless ? " and a WiFi firmware partition" : ""); + } + auto has_partition = [&](uint64_t id) { + return std::any_of(partitions->begin(), partitions->end(), [id](const partition_details &pd) { + return pd.has_id && pd.id == id; + }); + }; + if (!has_partition(FFS_DATA_PARTITION_ID)) { + fail(ERROR_NOT_POSSIBLE, "The device's partition table has no FFS partition (id 0x%016" PRIx64 ")", (uint64_t)FFS_DATA_PARTITION_ID); + } + if (uses_wireless && !has_partition(CYW43_FIRMWARE_PARTITION_ID)) { + fail(ERROR_NOT_POSSIBLE, "The device's partition table has no WiFi firmware partition (id 0x%016" PRIx64 ")", (uint64_t)CYW43_FIRMWARE_PARTITION_ID); + } + if (uses_wireless) { + // The firmware is stored as an image with a block loop, which the + // bootrom verifies - so one of the (A/B) partitions must have a valid one + picoboot_memory_access raw_access(con); + bool has_firmware = false; + for (auto &pd : *partitions) { + if (!pd.has_id || pd.id != CYW43_FIRMWARE_PARTITION_ID) continue; + partition_memory_access part_access(raw_access, pd.start); + vector bin; + try { + has_firmware = !find_all_blocks(part_access, bin).empty(); + } catch (failure_error &) { + // the block loop is not valid + } + if (has_firmware) break; + } + if (!has_firmware) { + fail(ERROR_NOT_POSSIBLE, "None of the device's WiFi firmware partitions contain firmware (a valid block loop)"); + } + } + + auto tmp = std::make_shared(); + auto file = get_rpi_connect_provision(); + *tmp << file->rdbuf(); + + auto program = get_iostream_memory_access(tmp, filetype::elf, true); + program.set_model(std::make_unique()); + + // {group, key, value} to configure; empty values keep the binary's defaults + std::vector> config = { + {"provision", "operation", operation}, + {"provision", "org_token", p.org_token}, + {"provision", "auth_key", p.auth_key}, + {"provision", "token", p.token}, + {"provision", "description", p.description}, + {"provision", "device_name", p.device_name}, + {"provision", "client_id", p.client_id}, + {"provision", "wifi_ssid", p.wifi_ssid}, + {"provision", "wifi_password", p.wifi_password}, + }; + for (auto &i : std::vector>{ + {"uart_config", "uart", p.uart}, + {"uart_config", "uart_tx", p.uart_tx}, + {"uart_config", "uart_rx", p.uart_rx}, + {"uart_config", "uart_baud", p.uart_baud}, + {"led_config", "led", p.led}, + {"cyw43_config", "wl_reg_on", p.wl_reg_on}, + {"cyw43_config", "wl_data_out", p.wl_data_out}, + {"cyw43_config", "wl_data_in", p.wl_data_in}, + {"cyw43_config", "wl_host_wake", p.wl_host_wake}, + {"cyw43_config", "wl_clock", p.wl_clock}, + {"cyw43_config", "wl_cs", p.wl_cs}}) { + if (std::get<2>(i) != provision_unset) { + config.emplace_back(std::get<0>(i), std::get<1>(i), std::to_string(std::get<2>(i))); + } + } + + // config_guts echoes each value, which would print the secrets + fos_ptr = fos_null_ptr; + try { + for (auto &c : config) { + if (std::get<2>(c).empty()) continue; + settings.config.group = std::get<0>(c); + settings.config.key = std::get<1>(c); + settings.config.value = std::get<2>(c); + config_guts(program); + } + } catch (...) { + fos_ptr = fos_base_ptr; + throw; + } + fos_ptr = fos_base_ptr; + +#if HAS_MBEDTLS + private_t private_key = {}; + public_t public_key = {}; + if (settings.seal.sign) read_keys(settings.filenames[0], &public_key, &private_key); + + elf_file source_file(settings.verbose); + elf_file *elf = &source_file; + elf->read_file(tmp); + sign_guts_elf(elf, private_key, public_key, program.get_model()); + auto out = std::make_shared(); + elf->write(out); + + auto signed_program = get_iostream_memory_access(out, filetype::elf, true); +#else + auto signed_program = get_iostream_memory_access(tmp, filetype::elf, true); +#endif + + fos << "Provisioning operation: " << operation << "\n"; + settings.load.execute = true; + load_guts(con, signed_program); + fos << "Provisioning output is on the device's USB and UART consoles. When done, its LED flashes " + "(slowly on success, quickly on failure) and it reboots to BOOTSEL.\n"; + + return true; +} + enum wl_type { wl_value, wl_bcd, diff --git a/rpi_connect_provision/BUILD.bazel b/rpi_connect_provision/BUILD.bazel new file mode 100644 index 00000000..e376e1c1 --- /dev/null +++ b/rpi_connect_provision/BUILD.bazel @@ -0,0 +1,19 @@ +load("@rules_cc//cc:cc_library.bzl", "cc_library") + +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "rpi_connect_provision_prebuilt", + srcs = ["rpi_connect_provision.elf"], +) + +# TODO: Make this work. +cc_library( + name = "rpi_connect_provision", + srcs = ["rpi_connect_provision.c"], + tags = ["manual"], + deps = [ + "//:rpi_connect_provision", + "@pico-sdk//src/rp2_common/pico_stdlib", + ], +) diff --git a/rpi_connect_provision/CMakeLists.txt b/rpi_connect_provision/CMakeLists.txt new file mode 100644 index 00000000..b706c35b --- /dev/null +++ b/rpi_connect_provision/CMakeLists.txt @@ -0,0 +1,116 @@ +cmake_minimum_required(VERSION 3.12) + +if (NOT USE_PRECOMPILED) + set(PICO_PLATFORM rp2350-arm-s) + # Pico 2 W, but RP2350B with maximum flash size - see the header + set(PICO_BOARD_HEADER_DIRS ${CMAKE_CURRENT_LIST_DIR}/boards) + set(PICO_BOARD rpi_connect_provision_board) + + set(PICO_NO_PICOTOOL 1) + + # If the user set these environment variables to influence the picotool + # build, unset them here so that they do not influence the pico-sdk + # build. This is especially required for flags that are not supported + # by arm-none-eabi compilers. + unset(ENV{CFLAGS}) + unset(ENV{CXXFLAGS}) + unset(ENV{LDFLAGS}) + + # Pull in SDK (must be before project) + include(${PICO_SDK_PATH}/external/pico_sdk_import.cmake) + + project(rpi_connect_provision C CXX ASM) + set(CMAKE_C_STANDARD 11) + set(CMAKE_CXX_STANDARD 17) + + if (PICO_SDK_VERSION_STRING VERSION_LESS "2.3.2") + message(FATAL_ERROR "Raspberry Pi Pico SDK version 2.3.2 (or later) required. Your version is ${PICO_SDK_VERSION_STRING}") + endif() + + # Initialize the SDK + pico_sdk_init() + + # One-shot provisioning image: the on-device equivalent of the host tool's + # provisioning operations (e.g. --create-device-identity). The operation and + # its inputs are set via binary info with picotool config. + add_executable(rpi_connect_provision + rpi_connect_provision.c + ) + target_link_libraries(rpi_connect_provision + pico_rpi_connect + pico_rpi_connect_ota + pico_stdlib + pico_cyw43_arch_lwip_poll + pico_cjson + pico_ffs + pico_flash_image + pico_lwip_mbedtls + pico_mbedtls + pico_rpi_connect_ota_default_config_headers + ) + # Reboots to bootsel after exiting, so you can flash the demo + target_compile_definitions(rpi_connect_provision PRIVATE + PICO_ENTER_USB_BOOT_ON_EXIT=1 + ) + # Both UART and USB, so progress can be followed over either + pico_enable_stdio_usb(rpi_connect_provision 1) + pico_enable_stdio_uart(rpi_connect_provision 1) + + # Use firmware partitions - but assume wifi firmware is already provisioned + target_compile_definitions(rpi_connect_provision PRIVATE CYW43_USE_FIRMWARE_PARTITION=1) + + target_compile_definitions(rpi_connect_provision PRIVATE + # --- Logging ---------------------------------------------------------- + # Default: show the flow of control (demo-app + library INFO and ERROR) + # without low-level debug spam. The libraries log nothing by default. + RPI_CONNECT_ERROR_ENABLE=1 + RPI_CONNECT_OTA_ERROR_ENABLE=1 + RPI_CONNECT_INFO_ENABLE=1 + RPI_CONNECT_OTA_INFO_ENABLE=1 + + # For data-limited or metered connections: check once for a pending deployment + # after boot sync instead of holding the SSE event stream open. + # RPI_CONNECT_OTA_DEMO_POLL_ONLY=1 + # If artefacts are hosted on a server that does not chain to the Pi + # Connect root CA, disable TLS verification for the download only; + # integrity then rests on the deployment's SHA-256 checksum. + # RPI_CONNECT_OTA_DEMO_UNVERIFIED_DOWNLOAD=1 + # For debugging, uncomment any of these to enable verbose logging in the + # libraries (or per-chunk progress etc. in the demo app): + # RPI_CONNECT_DEBUG_ENABLE=1 # core rpi_connect debug (incl. HTTP request layer) + # RPI_CONNECT_VERBOSE_DEBUG_ENABLE=1 # ...plus header/body dumps + # RPI_CONNECT_OTA_DEBUG_ENABLE=1 # OTA library debug + # HTTPC_DEBUG=LWIP_DBG_ON # lwIP HTTP client debug + # RPI_CONNECT_OTA_DEMO_DEBUG_ENABLE=1 # demo-app DEBUG (download progress) + + # User-Agent for all requests from the lwIP HTTP client + HTTPC_CLIENT_AGENT=\"rpi-connect-pico/${PICO_SDK_VERSION_MAJOR}.${PICO_SDK_VERSION_MINOR}\" + ALTCP_MBEDTLS_AUTHMODE=MBEDTLS_SSL_VERIFY_OPTIONAL + RPI_CONNECT_MBEDTLS_DEBUG_LEVEL=0 # mbedTLS debug verbosity (0=off, up to 4) + + # The TLS handshake nests under the synchronous request path; the 2K + # default overflows. + PICO_STACK_SIZE=0x1000 + ) + + pico_set_binary_type(rpi_connect_provision no_flash) + + # Strip the ELF, as it is embedded in picotool - keeping an unstripped copy for debugging + if (CMAKE_STRIP) + add_custom_command(TARGET rpi_connect_provision POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy $ ${CMAKE_CURRENT_BINARY_DIR}/rpi_connect_provision_unstripped.elf + COMMAND ${CMAKE_STRIP} --strip-all $ + COMMENT "Stripping rpi_connect_provision.elf" + VERBATIM) + endif() + + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/rpi_connect_provision.elf DESTINATION ${CMAKE_CURRENT_LIST_DIR}) +else() + project(rpi_connect_provision C CXX ASM) + message("Using precompiled rpi_connect_provision.elf") + configure_file(${CMAKE_CURRENT_LIST_DIR}/rpi_connect_provision.elf ${CMAKE_CURRENT_BINARY_DIR}/rpi_connect_provision.elf COPYONLY) + # Use manually specified variables + set(NULL ${CMAKE_MAKE_PROGRAM}) + set(NULL ${PICO_SDK_PATH}) + set(NULL ${PICO_DEBUG_INFO_IN_RELEASE}) +endif() diff --git a/rpi_connect_provision/boards/rpi_connect_provision_board.h b/rpi_connect_provision/boards/rpi_connect_provision_board.h new file mode 100644 index 00000000..af21931d --- /dev/null +++ b/rpi_connect_provision/boards/rpi_connect_provision_board.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +// ----------------------------------------------------- +// NOTE: THIS HEADER IS ALSO INCLUDED BY ASSEMBLER SO +// SHOULD ONLY CONSIST OF PREPROCESSOR DIRECTIVES +// ----------------------------------------------------- + +// Board for the provisioning image, which runs from RAM on any Pico 2 W style +// board: a Pico 2 W, but RP2350B with the maximum flash size, so it can reach +// partitions anywhere in flash on bigger parts (and use all 48 GPIOs). Pins +// that differ per board (UART, LED, wireless chip) are set at runtime through +// binary info instead. + +#ifndef _BOARDS_RPI_CONNECT_PROVISION_BOARD_H +#define _BOARDS_RPI_CONNECT_PROVISION_BOARD_H + +// --- FLASH --- +// 16MB is the most a single chip select can address +pico_board_cmake_set_default(PICO_FLASH_SIZE_BYTES, (16 * 1024 * 1024)) +#ifndef PICO_FLASH_SIZE_BYTES +#define PICO_FLASH_SIZE_BYTES (16 * 1024 * 1024) +#endif + +// --- CYW43 --- +// Wireless chip pins are set at runtime from binary info, for boards that use +// different pins to the Pico 2 W +#ifndef CYW43_PIN_WL_DYNAMIC +#define CYW43_PIN_WL_DYNAMIC 1 +#endif + +// --- FPGA --- +// Detect the FPGA at runtime, so the clocks (and so the UART baud rate) are +// correct there too - otherwise the PLLs are assumed and clk_peri is wrong +#ifndef PICO_NO_FPGA_CHECK +#define PICO_NO_FPGA_CHECK 0 +#endif + +#include "boards/pico2_w.h" + +// --- RP2350 VARIANT --- +// pico2_w.h sets this unconditionally +#undef PICO_RP2350A +#define PICO_RP2350A 0 + +#endif diff --git a/rpi_connect_provision/rpi_connect_provision.c b/rpi_connect_provision/rpi_connect_provision.c new file mode 100644 index 00000000..9e1d355e --- /dev/null +++ b/rpi_connect_provision/rpi_connect_provision.c @@ -0,0 +1,580 @@ +/** + * Copyright (c) 2025 Raspberry Pi (Trading) Ltd. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +/* On-device provisioning for Raspberry Pi Connect: the provisioning subset of + * the host tool's operations (main.c), run on the board itself so the OTP + * identity key is never handled on a host. + * + * The operation and its inputs are binary info pointer variables in the + * "provision" feature group, set with `picotool config` before the image is + * loaded: + * + * operation what to do (default create_identity): + * create_identity register the OTP identity key with an organisation + * (host --create-device-identity), and clear any stored + * access token so the new identity is used; needs + * org_token + * identity_exchange exchange the registered OTP identity for an access + * token and cache it in FFS (host + * --device-identity-exchange); checks the registration + * auth_key exchange a provisioning auth key for an access token + * and store it in FFS (host --auth); needs auth_key + * signin device-code signin: prints a code to enter in a + * browser, then stores the access token in FFS (host + * --signin) + * store_token store the given access token in FFS (host --token) + * clear delete the stored token and any deployment state + * wifi only store the WiFi credentials below + * org_token organisation token, for create_identity + * auth_key provisioning auth key, for auth_key + * token access token, for store_token + * description identity description, for create_identity + * device_name device name (default pico-) + * client_id Connect client UUID (default the library's) + * wifi_ssid if set, stored in FFS along with wifi_password + * wifi_password before the operation runs + * + * An operation fails if the secret it needs (org_token, auth_key or token) is + * not set. + * + * Console output goes to USB and to the UART in the "uart_config" group: + * uart UART instance, or -1 for no UART (default the board's) + * uart_tx, uart_rx UART pins, or -1 for none (default the board's) + * uart_baud UART baud rate (default 115200) + * + * The wireless chip's pins are in the "cyw43_config" group (default the + * board's): wl_reg_on, wl_data_out, wl_data_in, wl_host_wake, wl_clock, wl_cs + * + * When done, the LED in the "led_config" group flashes - slowly on success, + * quickly on failure - and then the board reboots to USB boot, ready for the + * demo image to be flashed: + * led GPIO of the LED, or -1 for the board's default LED + * (on the wireless chip for W boards) + */ + +#include +#include +#include + +#include "mbedtls/ecp.h" +#include "mbedtls/ctr_drbg.h" +#include "mbedtls/entropy.h" + +#include "connect_crypto.h" +#include "pico/binary_info.h" +#include "pico/bootrom.h" +#include "pico/cyw43_arch.h" +#include "pico/cyw43_driver.h" +#include "pico/ffs.h" +#include "pico/rpi_connect.h" +#include "pico/rpi_connect_ota.h" +#include "pico/stdlib.h" +#include "pico/unique_id.h" +#include "hardware/gpio.h" +#include "hardware/uart.h" + +// Longest organisation token that survives being formatted into the API's +// "Authorization: Bearer %s" header (a 256-byte buffer in rpi_connect.c). +#define ORG_TOKEN_SIZE 234 +// Access tokens are used in the same header +#define TOKEN_SIZE ORG_TOKEN_SIZE +#define AUTH_KEY_SIZE 128 +#define OPERATION_SIZE 32 +#define DESCRIPTION_SIZE 64 +#define DEVICE_NAME_SIZE 64 +#define CLIENT_ID_SIZE 40 +#define WIFI_SSID_SIZE 33 +#define WIFI_PASSWORD_SIZE 65 + +// LED flashing when done: half-period for success and failure, and how long +#define LED_SUCCESS_HALF_PERIOD_MS 500 +#define LED_FAILURE_HALF_PERIOD_MS 100 +#define LED_FLASH_MS 5000 + +// Device-code signin: how often to poll for the token, and for how long +#define SIGNIN_POLL_MS 3000 +// How often to repeat the code and URL, for a console connected after the start +#define SIGNIN_REPRINT_MS 15000 +#define SIGNIN_TIMEOUT_MS (15 * 60 * 1000) + +bi_decl(bi_program_feature_group(0x1111, 0x2222, "provision")); +bi_decl(bi_ptr_string(0x1111, 0x2222, operation, "create_identity", OPERATION_SIZE)); +bi_decl(bi_ptr_string(0x1111, 0x2222, org_token, "", ORG_TOKEN_SIZE)); +bi_decl(bi_ptr_string(0x1111, 0x2222, auth_key, "", AUTH_KEY_SIZE)); +bi_decl(bi_ptr_string(0x1111, 0x2222, token, "", TOKEN_SIZE)); +bi_decl(bi_ptr_string(0x1111, 0x2222, description, "Pico 2 W OTA demo", DESCRIPTION_SIZE)); +bi_decl(bi_ptr_string(0x1111, 0x2222, device_name, "", DEVICE_NAME_SIZE)); +bi_decl(bi_ptr_string(0x1111, 0x2222, client_id, "", CLIENT_ID_SIZE)); +bi_decl(bi_ptr_string(0x1111, 0x2222, wifi_ssid, "", WIFI_SSID_SIZE)); +bi_decl(bi_ptr_string(0x1111, 0x2222, wifi_password, "", WIFI_PASSWORD_SIZE)); + +bi_decl(bi_program_feature_group(0x1234, 0x5679, "uart_config")); +bi_decl(bi_ptr_int32(0x1234, 0x5679, uart, PICO_DEFAULT_UART)); +bi_decl(bi_ptr_int32(0x1234, 0x5679, uart_tx, PICO_DEFAULT_UART_TX_PIN)); +bi_decl(bi_ptr_int32(0x1234, 0x5679, uart_rx, PICO_DEFAULT_UART_RX_PIN)); +bi_decl(bi_ptr_int32(0x1234, 0x5679, uart_baud, PICO_DEFAULT_UART_BAUD_RATE)); + +bi_decl(bi_program_feature_group(0x1234, 0x567a, "cyw43_config")); +bi_decl(bi_ptr_int32(0x1234, 0x567a, wl_reg_on, CYW43_DEFAULT_PIN_WL_REG_ON)); +bi_decl(bi_ptr_int32(0x1234, 0x567a, wl_data_out, CYW43_DEFAULT_PIN_WL_DATA_OUT)); +bi_decl(bi_ptr_int32(0x1234, 0x567a, wl_data_in, CYW43_DEFAULT_PIN_WL_DATA_IN)); +bi_decl(bi_ptr_int32(0x1234, 0x567a, wl_host_wake, CYW43_DEFAULT_PIN_WL_HOST_WAKE)); +bi_decl(bi_ptr_int32(0x1234, 0x567a, wl_clock, CYW43_DEFAULT_PIN_WL_CLOCK)); +bi_decl(bi_ptr_int32(0x1234, 0x567a, wl_cs, CYW43_DEFAULT_PIN_WL_CS)); + +// Same group and key as xip_ram_perms, so picotool can set either the same way +bi_decl(bi_program_feature_group(0x1234, 0x5678, "led_config")); +bi_decl(bi_ptr_int32(0x1234, 0x5678, led, -1)); + +typedef enum { + OP_CREATE_IDENTITY, + OP_IDENTITY_EXCHANGE, + OP_AUTH_KEY, + OP_SIGNIN, + OP_STORE_TOKEN, + OP_CLEAR, + OP_WIFI, + OP_COUNT +} provision_op_t; + +static const char *const op_names[OP_COUNT] = { + [OP_CREATE_IDENTITY] = "create_identity", + [OP_IDENTITY_EXCHANGE] = "identity_exchange", + [OP_AUTH_KEY] = "auth_key", + [OP_SIGNIN] = "signin", + [OP_STORE_TOKEN] = "store_token", + [OP_CLEAR] = "clear", + [OP_WIFI] = "wifi", +}; + +static char serial_number[2*PICO_UNIQUE_BOARD_ID_SIZE_BYTES + 1]; +static bool cyw43_initialised; + +async_context_t *rpi_connect_default_async_context(void) { + return cyw43_arch_async_context(); +} + +// Secrets are only ever supplied via binary info +static int require_secret(const char *name, const char *value) { + if (!*value) { + printf("No %s given\n", name); + return -1; + } + return 0; +} + +static bool op_needs_network(provision_op_t op) { + return op == OP_CREATE_IDENTITY || op == OP_IDENTITY_EXCHANGE || + op == OP_AUTH_KEY || op == OP_SIGNIN; +} + +static int store_wifi_credentials(void) { + int rc = ffs_update_string(FFS_WIFI_SSID_FILE_ID, wifi_ssid); + if (rc == PICO_OK) { + rc = ffs_update_string(FFS_WIFI_PASSWORD_FILE_ID, wifi_password); + } + if (rc != PICO_OK) { + printf("Failed to store WiFi credentials (%d)\n", rc); + return -1; + } + printf("Stored WiFi credentials for %s\n", wifi_ssid); + return 0; +} + +static int connect_wifi(void) { + char *ssid = ffs_get_string(FFS_WIFI_SSID_FILE_ID); + char *password = ffs_get_string(FFS_WIFI_PASSWORD_FILE_ID); + int rc = -1; + int wifi_rc; + if (!ssid || !*ssid || !password) { + printf("No WiFi credentials in ffs\n"); + goto end; + } + + printf("Connecting to WiFi %s\n", ssid); + cyw43_arch_enable_sta_mode(); + wifi_rc = cyw43_arch_wifi_connect_timeout_ms(ssid, password, CYW43_AUTH_WPA2_AES_PSK, 30000); + if (wifi_rc) { + printf("Failed to connect to WiFi (%d)\n", wifi_rc); + goto end; + } + rpi_connect_set_async_context(rpi_connect_default_async_context()); + + // Signed requests carry X-Connect-Timestamp when the time is known; it is + // optional, so failure here is not fatal. + if (rpi_connect_update_time() <= 0) { + printf("Failed to get server time\n"); + } + rc = 0; +end: + free(ssid); + free(password); + return rc; +} + +static int store_token(const char *new_token) { + if (rpi_connect_ota_store_auth_token(new_token) != 0) { + printf("Failed to store access token\n"); + return -1; + } + printf("Stored access token\n"); + return 0; +} + +static int read_identity_key(unsigned char privkey[RPI_CONNECT_CRYPTO_P256_PRIVKEY_SIZE], bool create_if_empty) { + int ret = rpi_connect_ota_read_identity_key_otp(RPI_CONNECT_IDENTITY_OTP_ROW, privkey); + if (ret != 0) { + printf("Checking for existing key failed - rpi_connect_ota_read_identity_key_otp returned %d\n", ret); + return ret; + } + for (size_t i = 0; i < RPI_CONNECT_CRYPTO_P256_PRIVKEY_SIZE; i++) { + if (privkey[i] != 0) { + // Key is programmed + return 0; + } + } + + if (!create_if_empty) { + printf("No identity key in OTP - exiting\n"); + return -1; + } + + printf("No identity key in OTP - generating one\n"); + + mbedtls_ecp_keypair key; + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_entropy_context entropy; + + mbedtls_ecp_keypair_init(&key); + mbedtls_ctr_drbg_init(&ctr_drbg); + mbedtls_entropy_init(&entropy); + + ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, NULL, 0); + if (ret != 0) { + printf("Seeding entropy failed - mbedtls_ctr_drbg_seed returned %d\n", ret); + goto cleanup; + } + ret = mbedtls_ecp_gen_key(MBEDTLS_ECP_DP_SECP256R1, &key, mbedtls_ctr_drbg_random, &ctr_drbg); + if (ret != 0) { + printf("Generating key failed - mbedtls_ecp_gen_key returned %d\n", ret); + goto cleanup; + } + + ret = mbedtls_ecp_write_key(&key, privkey, RPI_CONNECT_CRYPTO_P256_PRIVKEY_SIZE); + if (ret != 0) { + printf("Reading key failed - mbedtls_ecp_write_key returned %d\n", ret); + goto cleanup; + } + +cleanup: + mbedtls_ecp_keypair_free(&key); + mbedtls_ctr_drbg_free(&ctr_drbg); + mbedtls_entropy_free(&entropy); + + if (ret != 0) { + return ret; + } + + printf("Private key generated - writing to OTP\n"); + otp_cmd_t cmd; + cmd.flags = RPI_CONNECT_IDENTITY_OTP_ROW | OTP_CMD_ECC_BITS | OTP_CMD_WRITE_BITS; + ret = rom_func_otp_access(privkey, RPI_CONNECT_CRYPTO_P256_PRIVKEY_SIZE, cmd); + if (ret) { + printf("ECC Write failed - rom_func_otp_access returned %d\n", ret); + return ret; + } + + // Re-read key out of OTP and return + return rpi_connect_ota_read_identity_key_otp(RPI_CONNECT_IDENTITY_OTP_ROW, privkey); +} + +static int create_identity(void) { + unsigned char privkey[RPI_CONNECT_CRYPTO_P256_PRIVKEY_SIZE]; + if (read_identity_key(privkey, true)) { + return -1; + } + + // Registering the public key derived from the OTP private key makes this + // identity, by construction, one the device can later prove it owns. + char *pubkey_pem = rpi_connect_crypto_ecdsa_p256_pubkey_pem(privkey); + if (!pubkey_pem) { + printf("Failed to derive public key\n"); + return -1; + } + + char *id = rpi_connect_create_device_identity(org_token, privkey, pubkey_pem, + description, device_name); + free(pubkey_pem); + if (!id) { + printf("Failed to create device identity\n"); + return -1; + } + printf("Created device identity %s for %s\n", id, device_name); + free(id); + + // A stored token takes priority over the identity in rpi_connect_ota_init, + // so remove any old one for the new identity to be used + int rc = ffs_delete(FFS_AUTH_TOKEN_FILE_ID); + if (rc != PICO_OK && rc != PICO_ERROR_NOT_FOUND) { + printf("Failed to clear stored access token (%d)\n", rc); + return -1; + } + return 0; +} + +static int identity_exchange(void) { + unsigned char privkey[RPI_CONNECT_CRYPTO_P256_PRIVKEY_SIZE]; + if (read_identity_key(privkey, false)) { + return -1; + } + + char *pubkey_pem = rpi_connect_crypto_ecdsa_p256_pubkey_pem(privkey); + if (!pubkey_pem) { + printf("Failed to derive public key\n"); + return -1; + } + + char *device_id = NULL; + char *new_token = rpi_connect_device_identity_exchange( + client_id, privkey, pubkey_pem, device_name, serial_number, &device_id); + free(pubkey_pem); + if (!new_token) { + printf("Device identity exchange failed\n"); + free(device_id); + return -1; + } + printf("Device identity exchanged for device %s\n", device_id ? device_id : "(null)"); + int rc = store_token(new_token); + free(new_token); + free(device_id); + return rc; +} + +static int auth_key_exchange(void) { + char *new_token = rpi_connect_handle_auth_key(auth_key, serial_number, device_name, client_id); + if (!new_token) { + printf("Auth key exchange failed\n"); + return -1; + } + int rc = store_token(new_token); + free(new_token); + return rc; +} + +static int signin(void) { + t_rpi_connect_signin *codes = rpi_connect_signin(client_id, serial_number); + if (!codes) { + printf("Failed to get signin information\n"); + return -1; + } + + int rc = -1; + absolute_time_t timeout = make_timeout_time_ms(SIGNIN_TIMEOUT_MS); + absolute_time_t next_print = get_absolute_time(); + while (!time_reached(timeout)) { + if (time_reached(next_print)) { + printf("Visit %s in your browser and enter the user code %s (%lld minutes left)\n", + codes->verification_uri_complete, codes->user_code, + (absolute_time_diff_us(get_absolute_time(), timeout) / 60000000) + 1); + stdio_flush(); + next_print = make_timeout_time_ms(SIGNIN_REPRINT_MS); + } + char *new_token = rpi_connect_retrieve_token_with_device_code( + client_id, codes->device_code, serial_number); + if (new_token) { + printf("Access token received\n"); + rc = store_token(new_token); + free(new_token); + break; + } + sleep_ms(SIGNIN_POLL_MS); + } + if (rc && time_reached(timeout)) { + printf("Timed out waiting for signin\n"); + } + rpi_connect_signin_cleanup(codes); + return rc; +} + +static int clear_state(void) { + // Deleting an absent file is not an error worth reporting + static const uint8_t file_ids[] = { + FFS_AUTH_TOKEN_FILE_ID, + FFS_DEPLOYMENT_ID_FILE_ID, + FFS_DEPLOYMENT_URI_FILE_ID, + FFS_DEPLOYMENT_CHECKSUM_FILE_ID, + FFS_DEPLOYMENT_STATUS_FILE_ID, + }; + for (size_t i = 0; i < count_of(file_ids); i++) { + ffs_delete(file_ids[i]); + } + printf("Cleared stored token and deployment state\n"); + return 0; +} + +static int init_cyw43(void) { + if (!cyw43_initialised) { + // Needs CYW43_PIN_WL_DYNAMIC, and must be set before the driver starts + uint pins[CYW43_PIN_INDEX_WL_COUNT] = { + [CYW43_PIN_INDEX_WL_REG_ON] = wl_reg_on, + [CYW43_PIN_INDEX_WL_DATA_OUT] = wl_data_out, + [CYW43_PIN_INDEX_WL_DATA_IN] = wl_data_in, + [CYW43_PIN_INDEX_WL_HOST_WAKE] = wl_host_wake, + [CYW43_PIN_INDEX_WL_CLOCK] = wl_clock, + [CYW43_PIN_INDEX_WL_CS] = wl_cs, + }; + int rc = cyw43_set_pins_wl(pins); + if (rc != PICO_OK) { + printf("Invalid cyw43 pins (%d)\n", rc); + return -1; + } + rc = cyw43_arch_init(); + if (rc) { + printf("Failed to initialise cyw43 (%d)\n", rc); + return -1; + } + cyw43_initialised = true; + } + return 0; +} + +static void init_stdio(void) { + stdio_usb_init(); + if (uart >= 0 && uart < NUM_UARTS) { + stdio_uart_init_full(uart_get_instance(uart), uart_baud, uart_tx, uart_rx); + } +} + +static bool led_is_wireless(void) { +#if defined(CYW43_WL_GPIO_LED_PIN) + return led < 0; +#else + return false; +#endif +} + +static void led_put(bool on) { +#if defined(CYW43_WL_GPIO_LED_PIN) + if (led_is_wireless()) { + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, on); + return; + } +#endif + gpio_put(led, on); +} + +// Flash the LED for a while to show the operation has finished +static void flash_led(bool success) { +#if !defined(CYW43_WL_GPIO_LED_PIN) && defined(PICO_DEFAULT_LED_PIN) + if (led < 0) { + led = PICO_DEFAULT_LED_PIN; + } +#endif + if (led_is_wireless()) { + // The wireless chip may not have been needed for the operation + if (init_cyw43()) { + return; + } + } else if (led >= 0 && led < NUM_BANK0_GPIOS) { + gpio_init(led); + gpio_set_dir(led, GPIO_OUT); + } else { + return; + } + + uint32_t half_period_ms = success ? LED_SUCCESS_HALF_PERIOD_MS : LED_FAILURE_HALF_PERIOD_MS; + for (uint32_t ms = 0; ms < LED_FLASH_MS; ms += 2 * half_period_ms) { + led_put(true); + sleep_ms(half_period_ms); + led_put(false); + sleep_ms(half_period_ms); + } +} + +static int provision(void) { + provision_op_t op; + for (op = 0; op < OP_COUNT; op++) { + if (strcmp(operation, op_names[op]) == 0) break; + } + if (op == OP_COUNT) { + printf("Unknown operation '%s'\n", operation); + return 1; + } + printf("Operation: %s\n", op_names[op]); + + pico_get_unique_board_id_string(serial_number, sizeof(serial_number)); + if (!*device_name) { + snprintf(device_name, sizeof(device_name), "pico-%s", serial_number); + } + if (!*client_id) { + snprintf(client_id, sizeof(client_id), "%s", rpi_connect_client_id()); + } + + int ffs_rc = ffs_initialise(); + if (ffs_rc != PICO_OK) { + printf("Failed to initialise ffs (%d)\n", ffs_rc); + return 1; + } + + if (*wifi_ssid && store_wifi_credentials()) { + return 1; + } else if (op == OP_WIFI && !*wifi_ssid) { + printf("No WiFi credentials given\n"); + return 1; + } + + // Check for missing secrets before the slow network setup + if ((op == OP_CREATE_IDENTITY && require_secret("org_token", org_token)) || + (op == OP_AUTH_KEY && require_secret("auth_key", auth_key)) || + (op == OP_STORE_TOKEN && require_secret("token", token))) { + return 1; + } + + if (op_needs_network(op)) { + if (init_cyw43() || connect_wifi()) { + return 1; + } + } + + int rc; + switch (op) { + case OP_CREATE_IDENTITY: rc = create_identity(); break; + case OP_IDENTITY_EXCHANGE: rc = identity_exchange(); break; + case OP_AUTH_KEY: rc = auth_key_exchange(); break; + case OP_SIGNIN: rc = signin(); break; + case OP_STORE_TOKEN: rc = store_token(token); break; + case OP_CLEAR: rc = clear_state(); break; + case OP_WIFI: + default: rc = 0; break; + } + return rc ? 1 : 0; +} + +// Work area for the bootrom's partition table load (needs at least 3264 bytes) +#define PT_WORKAREA_SIZE 0x1000 + +int main() { + init_stdio(); + rom_connect_internal_flash(); + rom_flash_exit_xip(); + rom_flash_enter_cmd_xip(); + + // Only needed for the load, so freed straight after (malloc is 8-byte aligned) + uint8_t *workarea = malloc(PT_WORKAREA_SIZE); + int rc = workarea ? rom_load_partition_table(workarea, PT_WORKAREA_SIZE, false) : PICO_ERROR_INSUFFICIENT_RESOURCES; + free(workarea); + if (rc) printf("Failed to load partition table (%d)\n", rc); + + rc = provision(); + printf("Provisioning %s\n", rc ? "failed" : "succeeded"); + flash_led(rc == 0); + + // Hand the board back in BOOTSEL, ready for the demo image to be flashed. + printf("Rebooting to USB boot\n"); + stdio_flush(); + + reset_usb_boot(0, 0); +} diff --git a/rpi_connect_provision/rpi_connect_provision.elf b/rpi_connect_provision/rpi_connect_provision.elf new file mode 100644 index 00000000..f1ffd3b5 Binary files /dev/null and b/rpi_connect_provision/rpi_connect_provision.elf differ