diff --git a/Cargo.lock b/Cargo.lock index 3f8e67b..0d0794d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -232,6 +232,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "az" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" + [[package]] name = "bare-metal" version = "0.2.5" @@ -290,7 +296,27 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.117", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.11.0", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.2", "shlex", "syn 2.0.117", ] @@ -374,6 +400,12 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + [[package]] name = "byteorder" version = "1.5.0" @@ -386,6 +418,12 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cbc" version = "0.1.2" @@ -666,8 +704,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-models" version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c406a8d3cdec6393dc8975b623d806ce2d586653c620f86f7fab2e043df1cb2" +source = "git+https://github.com/0x0ece/libcrux?rev=ee890a77#ee890a775bc3217d136a50c072be4eb7462459c5" dependencies = [ "hax-lib", "pastey", @@ -736,6 +773,17 @@ dependencies = [ "libc", ] +[[package]] +name = "crabgrind" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7459e07732f6de74001fcf73a3fdfbb0f368e4fd8e2392f4a2206a065e6e95" +dependencies = [ + "bindgen 0.72.1", + "cc", + "pkg-config", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1139,6 +1187,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-dma" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "994f7e5b5cb23521c22304927195f236813053eb9c065dd2226a32ba64695446" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "embedded-hal" version = "0.2.7" @@ -1175,6 +1232,12 @@ dependencies = [ "embedded-hal-async", ] +[[package]] +name = "embedded-storage" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032" + [[package]] name = "embedded-time" version = "0.12.1" @@ -1309,6 +1372,18 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixed" +version = "1.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af2cbf772fa6d1c11358f92ef554cb6b386201210bcf0e91fb7fba8a907fb40" +dependencies = [ + "az", + "bytemuck", + "half", + "typenum", +] + [[package]] name = "flate2" version = "1.1.9" @@ -2081,8 +2156,7 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libcrux-intrinsics" version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b254f1797aecd888f76e9647e6bec7b4c26fb6d60a73fd9856e4a1e535c704" +source = "git+https://github.com/0x0ece/libcrux?rev=ee890a77#ee890a775bc3217d136a50c072be4eb7462459c5" dependencies = [ "core-models", "hax-lib", @@ -2091,8 +2165,7 @@ dependencies = [ [[package]] name = "libcrux-macros" version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd6aa2dcd5be681662001b81d493f1569c6d49a32361f470b0c955465cd0338" +source = "git+https://github.com/0x0ece/libcrux?rev=ee890a77#ee890a775bc3217d136a50c072be4eb7462459c5" dependencies = [ "quote", "syn 2.0.117", @@ -2101,10 +2174,10 @@ dependencies = [ [[package]] name = "libcrux-ml-dsa" version = "0.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b067d3ea26eea867aad034b4567f7410ddf1807cab36aa82d3d610a18a06e70e" +source = "git+https://github.com/0x0ece/libcrux?rev=ee890a77#ee890a775bc3217d136a50c072be4eb7462459c5" dependencies = [ "core-models", + "crabgrind", "hax-lib", "libcrux-intrinsics", "libcrux-macros", @@ -2115,8 +2188,7 @@ dependencies = [ [[package]] name = "libcrux-platform" version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9e21d7ed31a92ac539bd69a8c970b183ee883872d2d19ce27036e24cb8ecc4" +source = "git+https://github.com/0x0ece/libcrux?rev=ee890a77#ee890a775bc3217d136a50c072be4eb7462459c5" dependencies = [ "libc", ] @@ -2124,17 +2196,16 @@ dependencies = [ [[package]] name = "libcrux-secrets" version = "0.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce650f3041b44ba40d4263852347d007cd2cd9d1cc856a6f6c8b2e10c3fd40b" +source = "git+https://github.com/0x0ece/libcrux?rev=ee890a77#ee890a775bc3217d136a50c072be4eb7462459c5" dependencies = [ + "crabgrind", "hax-lib", ] [[package]] name = "libcrux-sha3" version = "0.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f314521d5115afff6466e35e82cecd3b3bdf1dbed80d69285769a0f51c74fcc7" +source = "git+https://github.com/0x0ece/libcrux?rev=ee890a77#ee890a775bc3217d136a50c072be4eb7462459c5" dependencies = [ "hax-lib", "libcrux-intrinsics", @@ -2145,8 +2216,7 @@ dependencies = [ [[package]] name = "libcrux-traits" version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2613bf3dbf3670777bd6ecc3bcdd2d7a642663656b35ed2823529c4f1db0c9e9" +source = "git+https://github.com/0x0ece/libcrux?rev=ee890a77#ee890a775bc3217d136a50c072be4eb7462459c5" dependencies = [ "libcrux-secrets", "rand 0.10.1", @@ -2220,7 +2290,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae112b732df34c1420471e1ff21c562dfdf81219efb5960dd15b3947097e784a" dependencies = [ - "bindgen", + "bindgen 0.70.1", "cc", ] @@ -2447,6 +2517,68 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nrf-hal-common" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "089242b8b729622099054dcb5518e9eac80bcf6a7e30091c7cfa043383e9998c" +dependencies = [ + "cast", + "cfg-if", + "cortex-m", + "embedded-dma", + "embedded-hal 0.2.7", + "embedded-storage", + "fixed", + "nb 1.1.0", + "nrf-usbd", + "nrf52840-pac", + "rand_core 0.6.4", + "void", +] + +[[package]] +name = "nrf-nfc" +version = "2.2362.0" +dependencies = [ + "cortex-m", + "cty", + "defmt", +] + +[[package]] +name = "nrf-usbd" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945a178131ac5f69941dadb0d51c8e17cbe34cc09a0e2d51c099160a463751b8" +dependencies = [ + "cortex-m", + "usb-device", + "vcell", +] + +[[package]] +name = "nrf52840-hal" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c090616930ef132c1f7e9f9a964f6c8c5aa8962e402cc5abbbde7ce16ce9a0" +dependencies = [ + "embedded-hal 0.2.7", + "nrf-hal-common", + "nrf52840-pac", +] + +[[package]] +name = "nrf52840-pac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9efa9d1e910e3b825fd7e28f369363a8e036b386b77e21035b9f0e510de199cf" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "vcell", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -3354,6 +3486,7 @@ dependencies = [ "rtic-monotonics", "rtic-sync", "secrets-app", + "solo-apps", "static_cell", "systick-monotonic", "trussed", @@ -3373,12 +3506,67 @@ dependencies = [ "usbd-serial", ] +[[package]] +name = "runner-nrf52840dk" +version = "2.2362.0" +dependencies = [ + "admin-app", + "apdu-dispatch", + "cortex-m", + "cortex-m-rt", + "ctaphid-dispatch", + "defmt", + "defmt-rtt", + "embedded-hal 0.2.7", + "embedded-storage", + "embedded-time", + "fido-authenticator", + "generic-array 0.14.7", + "heapless 0.9.2", + "interchange", + "littlefs2", + "ndef-app", + "nrf-nfc", + "nrf-usbd", + "nrf52840-hal", + "nrf52840-pac", + "opcard", + "panic-halt", + "piv-authenticator", + "rtic", + "rtic-monotonics", + "rtic-sync", + "secrets-app", + "solo-apps", + "static_cell", + "trussed", + "trussed-auth", + "trussed-auth-backend", + "trussed-chunked", + "trussed-core", + "trussed-fs-info", + "trussed-hkdf", + "trussed-hpke", + "trussed-manage", + "trussed-staging", + "trussed-wrap-key-to-file", + "usb-device", + "usbd-ccid", + "usbd-ctaphid", +] + [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.2.3" @@ -3823,6 +4011,27 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "solo-apps" +version = "0.1.0" +dependencies = [ + "apdu-dispatch", + "ctaphid-dispatch", + "fido-authenticator", + "littlefs2", + "trussed", + "trussed-auth", + "trussed-auth-backend", + "trussed-chunked", + "trussed-core", + "trussed-fs-info", + "trussed-hkdf", + "trussed-hpke", + "trussed-manage", + "trussed-staging", + "trussed-wrap-key-to-file", +] + [[package]] name = "solo-pc" version = "2.2362.0" diff --git a/Cargo.toml b/Cargo.toml index 537b3f4..c7b99f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,12 +2,15 @@ members = [ "runners/lpc55", "runners/lpc55/board", + "runners/nrf52840dk", "runners/pc", "components/ndef-app", "components/nfc-device", "components/fm11nc08", + "components/nrf-nfc", "components/provisioner-app", "components/oath-export", + "components/solo-apps", "cli", ] # `cargo run` / `cargo build` / `cargo test` at the workspace root operate on the @@ -36,8 +39,10 @@ board = { path = "runners/lpc55/board" } ndef-app = { path = "components/ndef-app" } fm11nc08 = { path = "components/fm11nc08" } nfc-device = { path = "components/nfc-device" } +nrf-nfc = { path = "components/nrf-nfc" } provisioner-app = { path = "components/provisioner-app" } oath-export = { path = "components/oath-export" } +solo-apps = { path = "components/solo-apps" } # ── RTIC framework ──────────────────────────────────────────────────────────── rtic = "2.0.0" @@ -157,9 +162,13 @@ yasna = "0.6" codegen-units = 1 lto = true opt-level = "z" +panic = "abort" incremental = false debug = true +[profile.dev] +panic = "abort" + [profile.release.package.salty] opt-level = 2 @@ -177,3 +186,7 @@ trussed-staging = { git = "https://github.com/trussed-dev/trussed-staging.git", # Forked lpc55 (host) with the BCD CalVer-minor>999 version fix, used by the cli. lpc55 = { git = "https://github.com/0x0ece/lpc55-host", rev = "2a8a24e3df9ab9734f66ed2d7a56bc69101f531c" } apdu-dispatch = { git = "https://github.com/0x0ece/apdu-dispatch", rev = "3531b6b" } +# libcrux-ml-dsa with #[inline(always)] downgraded to #[inline] on the portable +# path — ~80 KB smaller .text on Cortex-M33 (no perf regression), lets all-apps + +# ML-DSA-44 fit on the LPC55. Only ml-dsa is patched; sha3/intrinsics unchanged. +libcrux-ml-dsa = { git = "https://github.com/0x0ece/libcrux", rev = "ee890a77" } diff --git a/components/nrf-nfc/Cargo.toml b/components/nrf-nfc/Cargo.toml new file mode 100644 index 0000000..738fb85 --- /dev/null +++ b/components/nrf-nfc/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "nrf-nfc" +version.workspace = true +edition.workspace = true +authors.workspace = true +description = "nRF52840 NFC stack: nrfx_nfct chip layer + libnfc_t4t.a Type 4 Tag library + Rust platform glue" +links = "nrf_nfc" + +[lib] +path = "src/lib.rs" + +[dependencies] +cortex-m = { workspace = true } +cty = "0.2" +defmt = { workspace = true } diff --git a/components/nrf-nfc/Makefile b/components/nrf-nfc/Makefile new file mode 100644 index 0000000..8a6e1fb --- /dev/null +++ b/components/nrf-nfc/Makefile @@ -0,0 +1,206 @@ +# Re-build the vendored static libraries + bindgen output for the +# nrf-nfc crate. Normal builds don't need this — `cargo build` reads +# the committed artifacts in vendor/ directly. +# +# Run `make regen` when bumping nrfx (the chip layer's source) or when +# Nordic ships a new libnfc_t4t.a (the closed-source blob in +# vendor/lib/libnfc_t4t.a — copy it in by hand from upstream nrfxlib +# first, then run regen to refresh the bindgen output). +# +# Requires: arm-none-eabi-gcc (in PATH) + bindgen-cli +# (cargo install bindgen-cli). + +# Pinned commits for the upstream sources we regenerate libnrfx_nfct.a +# from. Bump these together when upgrading the nrfx stack. +NRFX_COMMIT := aa83d4df8d5f41b591f23a8555794632afb3475d +CMSIS_COMMIT := f6226079fdb207e945dae7cea2c273ea5200376a + +VENDOR := vendor +BUILD := .regen-build + +.PHONY: regen regen-clean + +regen: regen-nrfx regen-t4t + +regen-clean: + rm -rf $(BUILD) + +# ── Embedded glue header bodies ────────────────────────────────────────────── +# +# These four headers are project-specific config nrfx requires at C +# compile + bindgen time. They live here (not in the crate tree) +# because the crate tree should contain only what the *normal* build +# needs, which doesn't include any C compilation. The regen target +# materializes them in $(BUILD)/glue, uses them, then leaves them for +# the next regen run (gitignored). + +define NRFX_CONFIG_H_BODY +#ifndef NRFX_CONFIG_H__ +#define NRFX_CONFIG_H__ +#ifndef NRF52840_XXAA +#define NRF52840_XXAA +#endif +#define NRFX_NFCT_ENABLED 1 +#define NRFX_NFCT_CONFIG_TIMER_INSTANCE_ID 4 +#define NRFX_NFCT_CONFIG_IRQ_PRIORITY 6 +#define NRFX_NFCT_CONFIG_LOG_ENABLED 0 +#define NRFX_TIMER_ENABLED 1 +#define NRFX_TIMER4_ENABLED 1 +#define NRFX_TIMER_DEFAULT_CONFIG_FREQUENCY 0 +#define NRFX_TIMER_DEFAULT_CONFIG_MODE 0 +#define NRFX_TIMER_DEFAULT_CONFIG_BIT_WIDTH 0 +#define NRFX_TIMER_DEFAULT_CONFIG_IRQ_PRIORITY 6 +#define NRFX_TIMER_CONFIG_LOG_ENABLED 0 +#include +#endif +endef +export NRFX_CONFIG_H_BODY + +define NRFX_GLUE_H_BODY +#ifndef NRFX_GLUE_H__ +#define NRFX_GLUE_H__ +#include +#define NRFX_ASSERT(e) ((void)(e)) +#define NRFX_STATIC_ASSERT(e) _Static_assert(e, #e) +#define NRFX_IRQ_PRIORITY_SET(i, p) NVIC_SetPriority((IRQn_Type)(i), p) +#define NRFX_IRQ_ENABLE(i) NVIC_EnableIRQ((IRQn_Type)(i)) +#define NRFX_IRQ_IS_ENABLED(i) (NVIC_GetEnableIRQ((IRQn_Type)(i)) != 0) +#define NRFX_IRQ_DISABLE(i) NVIC_DisableIRQ((IRQn_Type)(i)) +#define NRFX_IRQ_PENDING_SET(i) NVIC_SetPendingIRQ((IRQn_Type)(i)) +#define NRFX_IRQ_PENDING_CLEAR(i) NVIC_ClearPendingIRQ((IRQn_Type)(i)) +#define NRFX_IRQ_IS_PENDING(i) (NVIC_GetPendingIRQ((IRQn_Type)(i)) != 0) +extern uint8_t nrfx_critical_section_enter(void); +extern void nrfx_critical_section_exit(uint8_t saved); +#define NRFX_CRITICAL_SECTION_ENTER() { uint8_t _nrfx_cs = nrfx_critical_section_enter(); +#define NRFX_CRITICAL_SECTION_EXIT() nrfx_critical_section_exit(_nrfx_cs); } +extern void nrfx_delay_us(uint32_t us); +#define NRFX_DELAY_US(us) nrfx_delay_us(us) +#define NRFX_ATOMIC_FETCH_STORE(p, v) __atomic_exchange_n(p, v, __ATOMIC_SEQ_CST) +#define NRFX_ATOMIC_FETCH_OR(p, v) __atomic_fetch_or(p, v, __ATOMIC_SEQ_CST) +#define NRFX_ATOMIC_FETCH_AND(p, v) __atomic_fetch_and(p, v, __ATOMIC_SEQ_CST) +#define NRFX_ATOMIC_FETCH_XOR(p, v) __atomic_fetch_xor(p, v, __ATOMIC_SEQ_CST) +#define NRFX_ATOMIC_FETCH_ADD(p, v) __atomic_fetch_add(p, v, __ATOMIC_SEQ_CST) +#define NRFX_ATOMIC_FETCH_SUB(p, v) __atomic_fetch_sub(p, v, __ATOMIC_SEQ_CST) +#define NRFX_ATOMIC_CAS(p, ov, nv) ({ __typeof__(ov) _o = (ov); __atomic_compare_exchange_n(p, &_o, nv, 0, __ATOMIC_SEQ_CST, __ATOMIC_SEQ_CST); }) +#define NRFX_CLZ(v) __builtin_clz(v) +#define NRFX_CTZ(v) __builtin_ctz(v) +#define NRFX_POPCOUNT(v) __builtin_popcount(v) +#define NRFX_EVENT_READBACK_ENABLED 1 +#define NRFX_DPPI_CHANNELS_USED 0 +#define NRFX_DPPI_GROUPS_USED 0 +#define NRFX_PPI_CHANNELS_USED 0 +#define NRFX_PPI_GROUPS_USED 0 +#define NRFX_GPIOTE_CHANNELS_USED 0 +#define NRFX_EGUS_USED 0 +#define NRFX_TIMERS_USED 0 +#define NRFX_CUSTOM_ERROR_CODES 0 +#endif +endef +export NRFX_GLUE_H_BODY + +define NRFX_LOG_H_BODY +#ifndef NRFX_LOG_H__ +#define NRFX_LOG_H__ +#define NRFX_LOG_INFO(...) ((void)0) +#define NRFX_LOG_WARNING(...) ((void)0) +#define NRFX_LOG_ERROR(...) ((void)0) +#define NRFX_LOG_DEBUG(...) ((void)0) +#define NRFX_LOG_HEXDUMP_INFO(p, len) ((void)0) +#define NRFX_LOG_HEXDUMP_WARNING(p,len) ((void)0) +#define NRFX_LOG_HEXDUMP_ERROR(p, len) ((void)0) +#define NRFX_LOG_HEXDUMP_DEBUG(p, len) ((void)0) +#define NRFX_LOG_ERROR_STRING_GET(c) "" +#endif +endef +export NRFX_LOG_H_BODY + +define NRFX_WRAPPER_H_BODY +#include +#include +#include +endef +export NRFX_WRAPPER_H_BODY + +define T4T_WRAPPER_H_BODY +#define NRF52840_XXAA +#include +#include +endef +export T4T_WRAPPER_H_BODY + +# nrfx include flags reused by both compile + bindgen invocations. +NRFX_INCLUDES = \ + -I$(BUILD)/glue \ + -I$(BUILD)/cmsis/CMSIS/Core/Include \ + -I$(BUILD)/nrfx/drivers/include \ + -I$(BUILD)/nrfx/hal -I$(BUILD)/nrfx/haly \ + -I$(BUILD)/nrfx/helpers -I$(BUILD)/nrfx/templates \ + -I$(BUILD)/nrfx/bsp/stable -I$(BUILD)/nrfx/bsp/stable/mdk \ + -I$(BUILD)/nrfx/bsp/stable/soc -I$(BUILD)/nrfx/bsp/stable/soc/irqs \ + -I$(BUILD)/nrfx/bsp/stable/soc/interconnect \ + -I$(BUILD)/nrfx/bsp/stable/templates -I$(BUILD)/nrfx + +# ── regen-nrfx ────────────────────────────────────────────────────────────── +# +# Compile libnrfx_nfct.a from upstream sources + regenerate +# nrfx_nfct_bindings.rs. +regen-nrfx: $(BUILD)/nrfx/.git $(BUILD)/cmsis/.git + @command -v arm-none-eabi-gcc >/dev/null || { echo "arm-none-eabi-gcc not in PATH"; exit 1; } + @command -v bindgen >/dev/null || { echo "bindgen-cli not installed; run: cargo install bindgen-cli"; exit 1; } + @mkdir -p $(BUILD)/glue + @printf '%s\n' "$$NRFX_CONFIG_H_BODY" > $(BUILD)/glue/nrfx_config.h + @printf '%s\n' "$$NRFX_GLUE_H_BODY" > $(BUILD)/glue/nrfx_glue.h + @printf '%s\n' "$$NRFX_LOG_H_BODY" > $(BUILD)/glue/nrfx_log.h + @printf '%s\n' "$$NRFX_WRAPPER_H_BODY" > $(BUILD)/glue/wrapper.h + @echo "── Compiling libnrfx_nfct.a ──────────────────────────────────────────" + @for src in nrfx_nfct.c nrfx_timer.c; do \ + arm-none-eabi-gcc -c -mthumb -mcpu=cortex-m4 -mfloat-abi=soft \ + -DNRF52840_XXAA -D__HEAP_SIZE=0 -D__STACK_SIZE=0 \ + -fno-builtin -fno-strict-aliasing \ + -Wno-unused-parameter -Wno-expansion-to-defined -O2 \ + $(NRFX_INCLUDES) \ + -o $(BUILD)/$${src%.c}.o \ + $(BUILD)/nrfx/drivers/src/$${src} || exit 1; \ + done + @arm-none-eabi-ar rcs $(VENDOR)/lib/libnrfx_nfct.a \ + $(BUILD)/nrfx_nfct.o $(BUILD)/nrfx_timer.o + @echo "── Generating nrfx_nfct_bindings.rs ─────────────────────────────────" + @bindgen $(BUILD)/glue/wrapper.h \ + --use-core --ctypes-prefix cty --no-layout-tests --with-derive-default \ + --allowlist-function 'nrfx_nfct_.*' --allowlist-function 'nrf_nfct_.*' \ + --allowlist-type 'nrfx_nfct_.*' --allowlist-type 'nrf_nfct_.*' \ + --allowlist-var 'NRFX_NFCT_.*' --allowlist-var 'NRF_NFCT_.*' \ + --output $(VENDOR)/nrfx_nfct_bindings.rs \ + -- --target=thumbv7em-none-eabi -mthumb -mcpu=cortex-m4 -DNRF52840_XXAA \ + -isystem "$$(arm-none-eabi-gcc -print-sysroot)/include" \ + $(NRFX_INCLUDES) + +# ── regen-t4t ─────────────────────────────────────────────────────────────── +# +# Re-generate nfc_t4t_bindings.rs. The .a is Nordic's closed-source +# blob — copy a new vendor/lib/libnfc_t4t.a from upstream nrfxlib by +# hand before running this if you're bumping the version. +regen-t4t: + @command -v arm-none-eabi-gcc >/dev/null || { echo "arm-none-eabi-gcc not in PATH"; exit 1; } + @command -v bindgen >/dev/null || { echo "bindgen-cli not installed; run: cargo install bindgen-cli"; exit 1; } + @mkdir -p $(BUILD) + @printf '%s\n' "$$T4T_WRAPPER_H_BODY" > $(BUILD)/t4t_wrapper.h + @bindgen $(BUILD)/t4t_wrapper.h \ + --use-core --ctypes-prefix cty --no-layout-tests --with-derive-default \ + --allowlist-function 'nfc_t4t_.*' \ + --allowlist-type 'nfc_t4t_.*' \ + --allowlist-var 'NFC_T4T_.*' \ + --output $(VENDOR)/nfc_t4t_bindings.rs \ + -- --target=thumbv7em-none-eabi -mthumb -mcpu=cortex-m4 -DNRF52840_XXAA \ + -isystem "$$(arm-none-eabi-gcc -print-sysroot)/include" \ + -I$(VENDOR)/include + +$(BUILD)/nrfx/.git: + @mkdir -p $(BUILD) + git clone https://github.com/NordicSemiconductor/nrfx.git $(BUILD)/nrfx + cd $(BUILD)/nrfx && git checkout $(NRFX_COMMIT) + +$(BUILD)/cmsis/.git: + @mkdir -p $(BUILD) + git clone --filter=blob:none https://github.com/ARM-software/CMSIS_6 $(BUILD)/cmsis + cd $(BUILD)/cmsis && git checkout $(CMSIS_COMMIT) diff --git a/components/nrf-nfc/build.rs b/components/nrf-nfc/build.rs new file mode 100644 index 0000000..54e5968 --- /dev/null +++ b/components/nrf-nfc/build.rs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +// +// build.rs — link the vendored static archives. Both .a's plus their +// bindgen output live under ./vendor and are committed; this build does +// no C compilation and runs no bindgen. Regenerate via `make regen` +// when upstream sources change. + +use std::env; +use std::path::PathBuf; + +fn main() { + let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let lib_path = manifest.join("vendor/lib"); + println!("cargo:rustc-link-search=native={}", lib_path.display()); + println!("cargo:rustc-link-lib=static=nrfx_nfct"); + println!("cargo:rustc-link-lib=static=nfc_t4t"); + println!("cargo:rerun-if-changed=vendor/lib/libnrfx_nfct.a"); + println!("cargo:rerun-if-changed=vendor/lib/libnfc_t4t.a"); + println!("cargo:rerun-if-changed=vendor/nrfx_nfct_bindings.rs"); + println!("cargo:rerun-if-changed=vendor/nfc_t4t_bindings.rs"); +} diff --git a/components/nrf-nfc/src/lib.rs b/components/nrf-nfc/src/lib.rs new file mode 100644 index 0000000..ebc389f --- /dev/null +++ b/components/nrf-nfc/src/lib.rs @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +// +//! `nrf-nfc` — nRF52840 NFC stack. +//! +//! Two modules, each a thin Rust layer over a vendored static archive: +//! +//! - [`nrfx_nfct`] — chip-layer driver (Nordic's MIT-licensed nrfx_nfct.c +//! pre-compiled as `libnrfx_nfct.a`). +//! - [`nfc_t4t`] — ISO 14443-4 Type 4 Tag library (Nordic's closed-source +//! `libnfc_t4t.a`) plus the Rust platform-glue the library calls back +//! into. +//! +//! Both .a's and their bindgen outputs live in `vendor/`; see +//! `vendor/README.md` for provenance and `Makefile` for `make regen`. + +#![no_std] +#![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)] + +pub mod nfc_t4t; +pub mod nrfx_nfct; diff --git a/components/nrf-nfc/src/nfc_t4t.rs b/components/nrf-nfc/src/nfc_t4t.rs new file mode 100644 index 0000000..6c6ab95 --- /dev/null +++ b/components/nrf-nfc/src/nfc_t4t.rs @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: MIT +// +//! Type 4 Tag library — FFI to Nordic's closed-source `libnfc_t4t.a` +//! (vendored at `vendor/lib/libnfc_t4t.a`) plus the Rust platform-glue +//! the library calls back into. +//! +//! The public T4T API bindings are pre-generated and committed at +//! `vendor/nfc_t4t_bindings.rs`. +//! +//! `libnfc_t4t.a` links against six C-ABI symbols (per +//! `nm vendor/lib/libnfc_t4t.a | grep ' U '`): +//! +//! nfc_platform_setup +//! nfc_platform_nfcid1_default_bytes_get +//! nfc_platform_event_handler +//! nfc_platform_cb_request +//! nfc_platform_buffer_alloc +//! nfc_platform_buffer_free +//! +//! Modeled on Nordic's NCS reference (`nrf/subsys/nfc/lib/platform.c`, +//! BSD-3) without the Zephyr clock-control dependency: HFXO is started +//! once at boot in the runner and stays on, so we drive ACTIVATE +//! synchronously when a field is detected. +//! +//! The shims run in NFCT IRQ context (priority 4); the library trusts +//! pointer validity, and the buffer pool is single-flighted via the +//! in-use flag. + +include!("../vendor/nfc_t4t_bindings.rs"); + +// Each `pub unsafe extern "C" fn` below is an FFI entry point invoked +// solely by libnfc_t4t.a. The safety contract is fixed by the .a's +// calling code. +#[allow(clippy::missing_safety_doc)] +mod platform { + use core::ffi::c_void; + use core::ptr; + use core::sync::atomic::{AtomicBool, Ordering}; + + use crate::nrfx_nfct::{ + nrfx_nfct_evt_id_t_NRFX_NFCT_EVT_FIELD_DETECTED, + nrfx_nfct_evt_id_t_NRFX_NFCT_EVT_FIELD_LOST, nrfx_nfct_evt_t, nrfx_nfct_state_force, + nrfx_nfct_state_t_NRFX_NFCT_STATE_ACTIVATED, + }; + + /// Library callback resolver — libnfc_t4t.a hands us a function pointer + /// of this shape during `nfc_platform_setup`; we invoke it from + /// `nfc_platform_cb_request`. + pub type nfc_lib_cb_resolve_t = unsafe extern "C" fn(p_ctx: *const c_void, p_data: *const u8); + + // ── Per-tag DMA buffer pool (Type 4 Tag) ─────────────────────────────── + // + // `NFC_PLATFORM_T4T_BUFFER_SIZE = 259` from nfc_platform.h — sized to + // hold FSD=256 plus a 3-byte WTX frame. We give the library twice that + // for its TX+RX areas and single-flight allocations: the library owns + // the whole pool or none of it. + // + // 4-byte alignment for the EasyDMA-backed NFCT engine. The peripheral's + // register reference is silent on alignment, but every other EasyDMA + // peripheral on this part requires word alignment, so we honor the + // stricter constraint. + + const T4T_BUFFER_SIZE: usize = 259; + const T4T_TOTAL_BUF: usize = 2 * T4T_BUFFER_SIZE; + + #[repr(align(4))] + struct AlignedBuf([u8; T4T_TOTAL_BUF]); + + static mut S_T4T_BUF: AlignedBuf = AlignedBuf([0u8; T4T_TOTAL_BUF]); + static S_BUF_IN_USE: AtomicBool = AtomicBool::new(false); + + // Set by `nfc_platform_setup`, read by `nfc_platform_cb_request`. + // Single-writer (setup runs once at boot, in thread mode), single-reader + // (cb_request runs in IRQ). + static mut S_CB_RESOLVE: Option = None; + + // ── nfc_platform_setup ───────────────────────────────────────────────── + + #[no_mangle] + pub unsafe extern "C" fn nfc_platform_setup( + nfc_lib_cb_resolve: Option, + p_irq_priority: *mut u8, + ) -> i32 { + if nfc_lib_cb_resolve.is_none() || p_irq_priority.is_null() { + return -22; // -EINVAL + } + S_CB_RESOLVE = nfc_lib_cb_resolve; + *p_irq_priority = 6; + defmt::info!("nfc_platform: setup OK"); + 0 + } + + // ── nfc_platform_nfcid1_default_bytes_get ────────────────────────────── + // + // Derive default NFCID1 bytes from `FICR.DEVICEADDR` (universally + // present; FICR.NFC.TAGHEADER doesn't exist on every SoC). Byte 0 = + // 0x04 (NXP cascade tag) — without it iPhone classifies the device as + // a generic "fake" tag. Length must be 4, 7, or 10 per ISO 14443-3 + // NFCID1 sizes. + + const FICR_DEVICEADDR0: *const u32 = 0x1000_0060 as *const u32; + + #[no_mangle] + pub unsafe extern "C" fn nfc_platform_nfcid1_default_bytes_get( + p_buf: *mut u8, + buf_len: u32, + ) -> i32 { + if p_buf.is_null() { + return -22; // -EINVAL + } + if buf_len != 4 && buf_len != 7 && buf_len != 10 { + return -7; // -E2BIG + } + let a = ptr::read_volatile(FICR_DEVICEADDR0); + let b = ptr::read_volatile(FICR_DEVICEADDR0.offset(1)); + + *p_buf.offset(0) = 0x04; + *p_buf.offset(1) = (a >> 24) as u8; + *p_buf.offset(2) = (a >> 16) as u8; + *p_buf.offset(3) = (a >> 8) as u8; + + if buf_len >= 7 { + *p_buf.offset(4) = (b >> 24) as u8; + *p_buf.offset(5) = (b >> 16) as u8; + *p_buf.offset(6) = (b >> 8) as u8; + if buf_len == 10 { + *p_buf.offset(7) = b as u8; + *p_buf.offset(8) = a as u8; + *p_buf.offset(9) = ((a >> 16) ^ (b >> 16)) as u8; + } + } + defmt::info!( + "nfc_platform: nfcid_default len={} byte0={=u8:#x}", + buf_len, + *p_buf + ); + 0 + } + + // ── nfc_platform_event_handler ───────────────────────────────────────── + // + // libnfc_t4t.a forwards a subset of nrfx events so the platform can + // react to field transitions: + // + // 1. FIELD_DETECTED — drive NFCT to ACTIVATED. The library leaves this + // to the platform because the NCS reference gates it on HFXO + // becoming ready; our HFXO is permanently on, so we activate + // synchronously. + // + // 2. FIELD_LOST — restore FRAMEDELAYMAX to the spec default. The + // library widens it during ISO-DEP; without restoring, the next + // session's anti-coll runs in too wide a window and readers reject + // the response. + + // FRAMEDELAYMAX register, fixed by the SVD; see nRF52840 product + // spec §6.18 (NFCT). + const NRF_NFCT_FRAMEDELAYMAX: *mut u32 = 0x4000_5508 as *mut u32; + const FRAMEDELAYMAX_SPEC_DEFAULT: u32 = 0x1000; + + #[no_mangle] + pub unsafe extern "C" fn nfc_platform_event_handler(p_event: *const nrfx_nfct_evt_t) { + let id = (*p_event).evt_id; + if id == nrfx_nfct_evt_id_t_NRFX_NFCT_EVT_FIELD_DETECTED { + defmt::info!("nfc_platform: FIELD_DETECTED"); + nrfx_nfct_state_force(nrfx_nfct_state_t_NRFX_NFCT_STATE_ACTIVATED); + } else if id == nrfx_nfct_evt_id_t_NRFX_NFCT_EVT_FIELD_LOST { + defmt::info!("nfc_platform: FIELD_LOST (FRAMEDELAYMAX restored)"); + ptr::write_volatile(NRF_NFCT_FRAMEDELAYMAX, FRAMEDELAYMAX_SPEC_DEFAULT); + } else { + defmt::info!("nfc_platform: nrfx evt {=u32:#x}", id); + } + } + + // ── nfc_platform_cb_request ──────────────────────────────────────────── + // + // libnfc_t4t.a uses this to dispatch deferred callbacks. We invoke the + // resolver directly (no scheduling), so the library callback runs in + // the NFCT IRQ. + + #[no_mangle] + pub unsafe extern "C" fn nfc_platform_cb_request( + p_ctx: *const c_void, + _ctx_len: usize, + p_data: *const u8, + _data_len: usize, + _copy_data: bool, + ) { + defmt::info!( + "nfc_platform: cb_request ctx={=u32:#x} data={=u32:#x}", + p_ctx as u32, + p_data as u32 + ); + if let Some(resolve) = S_CB_RESOLVE { + resolve(p_ctx, p_data); + } + } + + // ── nfc_platform_buffer_alloc / free ─────────────────────────────────── + + #[no_mangle] + pub unsafe extern "C" fn nfc_platform_buffer_alloc(size: usize) -> *mut u8 { + let in_use = S_BUF_IN_USE.load(Ordering::SeqCst); + defmt::info!( + "nfc_platform: buf_alloc size={=usize} in_use={=bool}", + size, + in_use + ); + if in_use || size > T4T_TOTAL_BUF { + return ptr::null_mut(); + } + S_BUF_IN_USE.store(true, Ordering::SeqCst); + core::ptr::addr_of_mut!(S_T4T_BUF.0) as *mut u8 + } + + #[no_mangle] + pub unsafe extern "C" fn nfc_platform_buffer_free(p_buf: *mut u8) { + defmt::info!("nfc_platform: buf_free p={=u32:#x}", p_buf as u32); + let our_buf = core::ptr::addr_of_mut!(S_T4T_BUF.0) as *mut u8; + if p_buf == our_buf { + S_BUF_IN_USE.store(false, Ordering::SeqCst); + } + } +} diff --git a/components/nrf-nfc/src/nrfx_nfct.rs b/components/nrf-nfc/src/nrfx_nfct.rs new file mode 100644 index 0000000..34a6c88 --- /dev/null +++ b/components/nrf-nfc/src/nrfx_nfct.rs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +// +//! Chip-layer driver — FFI to Nordic's MIT-licensed `nrfx_nfct.c`, +//! pre-compiled into `vendor/lib/libnrfx_nfct.a`. +//! +//! The bindgen output is committed at `vendor/nrfx_nfct_bindings.rs`; +//! this module just `include!`s it and adds two C-ABI helpers the +//! driver needs at runtime (critical-section + microsecond delay). +//! +//! IRQ handlers `nrfx_nfct_irq_handler` and +//! `nrfx_nfct_workaround_timer_handler` are exported by the .a; the +//! runner's RTIC `binds = NFCT` / `binds = TIMER4` tasks call them. + +// Pre-generated by bindgen — see vendor/README.md and the regen target. +include!("../vendor/nrfx_nfct_bindings.rs"); + +// ── Critical section ───────────────────────────────────────────────────── +// +// nrfx's critical-section convention: enter() returns a state that exit() +// restores. We mirror PRIMASK behavior so nested critical sections nest +// correctly: enter() saves the current PRIMASK and disables interrupts; +// exit() writes the saved value back. +// +// We avoid `cortex_m::interrupt::free` because that closure-based API +// doesn't compose with the C macro pattern (enter/exit on different lines). + +#[unsafe(no_mangle)] +pub extern "C" fn nrfx_critical_section_enter() -> u8 { + let primask = cortex_m::register::primask::read(); + cortex_m::interrupt::disable(); + if primask.is_active() { + 0 + } else { + 1 + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn nrfx_critical_section_exit(saved: u8) { + if saved == 0 { + // PRIMASK was clear (interrupts enabled) before enter() — re-enable. + unsafe { cortex_m::interrupt::enable() }; + } +} + +// ── Microsecond delay (busy loop, calibrated for 64 MHz core) ──────────── + +#[unsafe(no_mangle)] +pub extern "C" fn nrfx_delay_us(us: u32) { + // cortex_m::asm::delay takes cycles; at 64 MHz, 64 cycles = 1 µs. + cortex_m::asm::delay(us.saturating_mul(64)); +} diff --git a/components/nrf-nfc/vendor/LICENSE b/components/nrf-nfc/vendor/LICENSE new file mode 100644 index 0000000..d2ab383 --- /dev/null +++ b/components/nrf-nfc/vendor/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2017 - 2026, Nordic Semiconductor ASA +All rights reserved. + +SPDX-License-Identifier: BSD-3-Clause + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/components/nrf-nfc/vendor/README.md b/components/nrf-nfc/vendor/README.md new file mode 100644 index 0000000..004e0d1 --- /dev/null +++ b/components/nrf-nfc/vendor/README.md @@ -0,0 +1,44 @@ +# Vendored NFC artifacts + +Pre-built artifacts the `nrf-nfc` crate links against. Both `.a`'s and +their bindgen outputs are committed so the normal build needs no nrfx +clone, no CMSIS clone, and no C cross-compiler. + +## libnrfx_nfct.a (chip layer) + +Compiled from Nordic's MIT-licensed +[nrfx](https://github.com/NordicSemiconductor/nrfx) NFCT + TIMER +drivers. Source commits and build flags are pinned in `../Makefile`. + +| File | Description | +|---|---| +| `lib/libnrfx_nfct.a` | nrfx_nfct.c + nrfx_timer.c, compiled for thumbv7em-none-eabihf at -O2 | +| `nrfx_nfct_bindings.rs` | bindgen output for nrfx_nfct.h + nrf_nfct.h | + +## libnfc_t4t.a (Type 4 Tag library) + +Verbatim copy of Nordic's closed-source Type-4-Tag library (ISO 14443-4 +ISO-DEP — anti-collision, RATS/ATS, I-block chaining, WTX) from +[nrfconnect/sdk-nrfxlib](https://github.com/nrfconnect/sdk-nrfxlib) at +commit `529012899ffb2aa8ef69cbbb315eaf2848737aca`. + +| File | Source path in nrfxlib | +|---|---| +| `lib/libnfc_t4t.a` | `nfc/lib/cortex-m4/hard-float/libnfc_t4t.a` | +| `include/nfc_t4t_lib.h` | `nfc/include/nfc_t4t_lib.h` | +| `include/nrf_nfc_errno.h` | `nfc/include/nrf_nfc_errno.h` | +| `LICENSE` | top-level `LICENSE` (Nordic 5-Clause) | +| `nfc_t4t_bindings.rs` | bindgen output for nfc_t4t_lib.h | + +The library's `nfc_platform.h` C-callback contract is implemented in +Rust at [`../src/nfc_t4t/nfc_platform.rs`](../src/nfc_t4t/nfc_platform.rs); +the header itself is not vendored. + +`include/` is only consumed at regen time (by bindgen-cli), not by +the normal build. + +## Regenerating + +When bumping upstream versions, from this crate's directory run +`make regen` (requires `arm-none-eabi-gcc` and `bindgen-cli`). The +artifacts under this directory get rewritten in place; commit them. diff --git a/components/nrf-nfc/vendor/include/nfc_t4t_lib.h b/components/nrf-nfc/vendor/include/nfc_t4t_lib.h new file mode 100644 index 0000000..d952ee4 --- /dev/null +++ b/components/nrf-nfc/vendor/include/nfc_t4t_lib.h @@ -0,0 +1,394 @@ +/** + * Copyright (c) 2016, Telit Communications Cyprus Ltd + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form, except as embedded into a Nordic + * Semiconductor ASA integrated circuit in a product or a software update for + * such product, must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other + * materials provided with the distribution. + * + * 3. Neither the name of Nordic Semiconductor ASA nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * 4. This software, with or without modification, must only be used with a + * Nordic Semiconductor ASA integrated circuit. + * + * 5. Any software provided in binary form under this license must not be reverse + * engineered, decompiled, modified and/or disassembled. + * + * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + + +#ifndef NFC_T4T_LIB_H__ +#define NFC_T4T_LIB_H__ + +/** @file nfc_t4t_lib.h + * + * + * @defgroup nfc_t4t NFC Type 4 Tag + * @brief Implementation of NFC Type 4 Tag. + * + * @defgroup nfc_t4t_lib NFC tag 4 type emulation library + * @{ + * @ingroup nfc_t4t + * @brief The T4T emulation library interface + * + * This is the NFC Forum NDEF tag 4 type emulation library. It implements the + * ISO14443-4A protocol (ISO-DEP) and additionally can emulate a read-writable + * NDEF content. If the emulation of the NDEF content is not needed, the library + * works in a raw mode where all APDUs are delivered to the user, who is then + * responsible to generate a timely RPDU as a response. + * + * The sequence of initializing functions determines whether the NDEF emulation + * will run or whether the raw mode is used. + * + * - E.g. NDEF emulation + * * @ref nfc_t4t_setup + * * @ref nfc_t4t_ndef_rwpayload_set or + @ref nfc_t4t_ndef_staticpayload_set + * * @ref nfc_t4t_emulation_start + * * ... running in NDEF emulation mode + * - E.g. RAW mode + * * @ref nfc_t4t_setup + * * @ref nfc_t4t_emulation_start + * * ... running in RAW emulation mode + * + * @note If you are using nRF52832 chip (in IC rev. Engineering B or + * Engineering C) or if You are using nRF52840 chip (in IC rev. Engineering A, + * B or C) library will use TIMER 4 to apply workarounds for the anomalies. + */ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define NFC_T4T_MAX_PAYLOAD_SIZE 0xFFF0U + +/**< @brief Emulation mode. */ +typedef enum +{ + NFC_T4T_EMUMODE_NDEF, /**< Emulated NDEF AID and EF-Files. */ + NFC_T4T_EMUMODE_PICC /**< Run just ISO-DEP, deliver I-Frames up. */ +} nfc_t4t_emu_mode_t; + +/**< @brief Event identifiers used by the @ref nfc_t4t_callback_t */ +typedef enum +{ + NFC_T4T_EVENT_NONE, + /**< This ID is never used. Dummy value for completeness. */ + + NFC_T4T_EVENT_FIELD_ON, + /**< External Reader polling detected. */ + + NFC_T4T_EVENT_FIELD_OFF, + /**< External Reader polling ended. */ + + NFC_T4T_EVENT_NDEF_READ, + /**< External Reader has read static NDEF-Data from Emulation. */ + /**< + * A Read operation happened on last byte of NDEF-Data. + */ + + NFC_T4T_EVENT_NDEF_UPDATED, + /**< External Reader has written to length information of NDEF-Data from + * Emulation. + */ + /**< + * The usual behavior of a Reader-Writer that accesses NDEF information + * for update is to set the length to zero at the beginning of the + * update process. It then writes the content of NDEF-Data. When all + * content is written it will update the length information inside the + * NDEF file. This event will be generated every time an update to the + * length is happening. This length information is residing in the first + * 2 bytes of the NDEF-Content container and is called 'NLEN'. Since + * this callback is triggered on any access to these bytes the returned + * data_length information might not be consistent (e.g. in case of only + * a single byte write to the length). + * + * @param data Pointer to current data of NDEF message + * @param data_length Current value of NDEF content length information + * i.e. 'NLEN' field. + */ + + NFC_T4T_EVENT_DATA_TRANSMITTED, + /**< In Raw mode it signals that the data from @ref + * nfc_t4t_response_pdu_send have been sent out. + */ + + NFC_T4T_EVENT_DATA_IND, + /**< In Raw mode delivers the APDU fragments */ + /**< + * All @ref NFC_T4T_EVENT_DATA_IND events that have the @ref + * NFC_T4T_DI_FLAG_MORE flag set belong to the same APDU. The first + * @ref NFC_T4T_EVENT_DATA_IND without @ref NFC_T4T_DI_FLAG_MORE + * flag signals the completeness of the APDU. The Application then has + * to reply with a call to @ref nfc_t4t_response_pdu_send. The library + * will handle internally the fragmentation of the response towards the + * Reader-Writer. The data of the response PDU must be kept valid until + * the next callback from the library happens (e.g. next @ref + * NFC_T4T_EVENT_DATA_IND or @ref NFC_T4T_EVENT_FIELD_OFF). + * + * @param data Pointer to the fragment of APDU. + * @param data_length Length of data. + * @param flags nfc_t4t_data_ind_flags. + */ +} nfc_t4t_event_t; + +/**< @brief Flags coming with nfc_t4t_callback_t at @ref + * NFC_T4T_EVENT_DATA_IND event. + */ +typedef enum +{ + NFC_T4T_DI_FLAG_NONE = 0x00, /**< Dummy value. */ + NFC_T4T_DI_FLAG_MORE = 0x01 + /**< This signals that more data is expected to be received. */ +} nfc_t4t_data_ind_flags_t; + +/**< @brief Maximum values for the Frame Wait time Integer. */ +typedef enum +{ + NFC_T4T_FWI_MAX_VAL_EMV = 0x07, /**< Value specified by the EMV specification. */ + NFC_T4T_FWI_MAX_VAL_NFC = 0x08 /**< Value specified by the NFC specification. */ +} nfc_t4t_fwi_max_val_t; + +/**< @brief Parameter IDs that can be set/get with @ref nfc_t4t_parameter_set or + * @ref nfc_t4t_parameter_get. + */ +typedef enum +{ + NFC_T4T_PARAM_FWI, + /**< Frame Wait Time parameter. + * The maximum allowed value is limited by the NFC_T4T_PARAM_FWI_MAX parameter. + * In case of nRF52832 the maximum allowed value is 4, + * setting the parameter higher will result in silent truncation to 4. + */ + + NFC_T4T_PARAM_FDT_MIN, + /**< Frame Delay Time Min parameter + * The parameter controls the frame transmission timing during collision resolution. + */ + + NFC_T4T_PARAM_SELRES, + /**< Parameter for setting 'Protocol' bits for SEL_RES packet */ + + NFC_T4T_PARAM_NFCID1, + /**< NFCID1 value, data can be 4, 7, or 10 bytes long (single, double, + * or triple size). To use default NFCID1 of specific length pass one + * byte containing requested length. Default 7-byte NFCID1 will be + * used if this parameter was not set. This parameter can be set + * before nfc_t2t_setup() to set initial NFCID1 and it can be changed + * later. + */ + + NFC_T4T_PARAM_FWI_MAX, + /**< Maximum value for the Frame Wait time Integer. + * Valid parameter values are 7 and 8. + * The default value is 8. + * This parameter can be used to limit the maximum FWI value to 7, + * to align the implementation with the EMV specifications. + */ +} nfc_t4t_param_id_t; + +/** @brief Callback to pass events from NFCLib to application. + * + * @param context Application context for callback execution. + * @param event The event that occurred. see nfc_t4t_event. + * @param data Data to send to the application (event specific). + * @param data_length Length of the data. In case of @ref + * NFC_T4T_EVENT_NDEF_UPDATED, this parameter contains the value of the 'NLEN' + * field of the NDEF File; if the value is non-zero, it corresponds to the new + * size of the NDEF Message in the updated NDEF File. + * @param flags Some events deliver flags. see nfc_t4t_event for details. + */ +typedef void (*nfc_t4t_callback_t)(void *context, + nfc_t4t_event_t event, + const uint8_t *data, + size_t data_length, + uint32_t flags); + +/** @brief Register the application callback for event signaling. + * + * The callback will be called by NFCLib to notify the application of relevant + * events. It will be called from the HAL_NFC callback context. The library + * support 3 different Modes of Emulation: + * - Raw ISO-Dep exchanges. All PDUs are signaled through the callback. + * - Read-Only T4T NDEF-Tag. A static buffer is served. Only Field-Status + * callbacks. + * - Read-Write T4T NDEF-Tag. A mutable buffer is used. Only Field-Status + * callbacks. + * + * The default mode is Raw ISO-Dep mode. The two other NDEF T4T modes are + * activated through the corresponding @ref nfc_t4t_ndef_rwpayload_set/ @ref + * nfc_t4t_ndef_staticpayload_set functions. The mode is locked in with a call + * to @ref nfc_t4t_emulation_start. + * + * @param callback Function pointer to the callback. + * @param context Pointer to a memory area used by the callback for execution + * (optional). + * + * @retval 0 Success. + * @retval -NRF_EINVAL Invalid argument (e.g. wrong data length, NULL pointer)) + * @retval -NRF_EOPNOTSUPP If emulation is in running state. + */ +int nfc_t4t_setup(nfc_t4t_callback_t callback, void *context); + +/** @brief Set emulation buffer and content for a NDEF Tag emulation that is + * Read/Writable. + * + * The buffer needs to be kept accessible for the lifetime of the emulation. + * If an external Reader-Writer changes the NDEF content it is signaled through + * the app-callback. Buffer can be changed during the lifetime of the emulation, + * when NDEF READ or UPDATE procedure is pending, and it will be changed after + * this procedure has completed. To perform this procedure safely, make sure to + * disable NFC interrupts. + * + * @param emulation_buffer Buffer pointer + * @param buffer_length Length of buffer (maximum writable NDEF size) + * + * @retval 0 Success. + * @retval -NRF_EINVAL Invalid argument (for example, wrong data length, NULL pointer). + * @retval -NRF_EOPNOTSUPP If the new buffer has a different length than the first one. + * @retval -NRF_EFAULT If the provided buffer is the currently used buffer. + */ +int nfc_t4t_ndef_rwpayload_set(uint8_t *emulation_buffer, + size_t buffer_length); + +/** @brief Set emulation buffer and content for an NDEF Tag emulation that is + * Read-Only. + * + * The buffer needs to be kept accessible for the lifetime of the emulation. + * Since no write access is done to the buffer, the content could reside in + * flash memory. + * + * @param emulation_buffer Const buffer pointer + * @param buffer_length Length of contained NDEF payload message + * + * @retval 0 Success. + * @retval -NRF_EINVAL Invalid argument (for example, wrong data length, NULL pointer). + * @retval -NRF_EOPNOTSUPP Emulation is in running stated. + */ +int nfc_t4t_ndef_staticpayload_set(const uint8_t *emulation_buffer, + size_t buffer_length); + +/** @brief Send a raw response PDU after getting a Request PDU callback. + * + * When the library works in raw ISO-DEP mode it will signal request PDUs + * through the callback. The application then has to answer with a response PDU. + * It will use this function to send back the response PDU. This function can + * not be used in T4T NDEF (RW / STATIC) emulation modes. + * + * The lower ISODEP layer will handle the defragmentation of a long response PDU + * into smalleR pieces that the PCD can understand. + * + * @param pdu Const PDU pointer. + * @param pdu_length Length of PDU. + * + * @retval 0 Success. + * @retval -NRF_EINVAL Invalid argument (for example, wrong data length, NULL pointer). + * @retval -NRF_EOPNOTSUPP Emulation is in running state. + */ +int nfc_t4t_response_pdu_send(const uint8_t *pdu, size_t pdu_length); + +/** @brief Set an NFC parameter. + * + * Allows to set an NFC configuration parameter. + * + * @param id ID of the parameter to set. + * @param data Pointer to a buffer containing the data to set. + * @param data_length Size of the buffer containing the data to set. + * + * @retval 0 Success. + * @retval -NRF_EINVAL Invalid argument (for example, wrong data length, NULL pointer). + */ +int nfc_t4t_parameter_set(nfc_t4t_param_id_t id, + void *data, + size_t data_length); + +/** @brief Query an NFC parameter value. + * + * The queried value will be placed into the passed data buffer. + * If the buffer is too small, max_data_length will contain the required + * buffer size. If the buffer is big enough, max_data_length will contain + * the actual size of the data. + * + * @param id ID of the parameter to query. + * @param data Pointer to a buffer receiving the queried data. + * @param max_data_length Size of the buffer, receives actual size of queried + * data. + * + * @retval 0 Success. + * @retval -NRF_EINVAL Invalid argument (for example, wrong data length, NULL pointer). + */ +int nfc_t4t_parameter_get(nfc_t4t_param_id_t id, + void *data, + size_t *max_data_length); + +/** @brief Activate the NFC frontend. + * + * Only after calling this function, events will be posted to the application + * callback. + * + * @retval 0 Success. + * @retval -NRF_EOPNOTSUPP Already started. + */ +int nfc_t4t_emulation_start(void); + +/** + * @brief Deactivate the NFC frontend. + * + * After calling this function, no more events will be posted to the application + * callback. + * + * @retval 0 Success. + * @retval -NRF_EOPNOTSUPP Emulation was already stopped. + */ +int nfc_t4t_emulation_stop(void); + +/** + * @brief Release reference to application callback. + * + * After calling this function, the passed callback pointer is no longer + * considered valid. + * After calling this function, the passed ndef pointer is no longer + * considered valid. + * + * You need to restart with @ref nfc_t4t_setup to run a new Emulation. + * + * @retval 0 Success. + * @retval -NRF_EOPNOTSUPP The NFC T4T has been de-initialized already. + */ +int nfc_t4t_done(void); + +#ifdef __cplusplus +} +#endif + +/** + * @} + */ + +#endif /** NFC_T4T_LIB_H__ */ diff --git a/components/nrf-nfc/vendor/include/nrf_nfc_errno.h b/components/nrf-nfc/vendor/include/nrf_nfc_errno.h new file mode 100644 index 0000000..bf37743 --- /dev/null +++ b/components/nrf-nfc/vendor/include/nrf_nfc_errno.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2021 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause + */ + +#ifndef NRF_NFC_ERRNO_H__ +#define NRF_NFC_ERRNO_H__ + +/** + * @defgroup nrf_nfc_errno Error codes for nRF NFC libraries. + * @{ + * + * @brief Defines error codes that can be used in nRF NFC libraries. + */ + + +#ifdef __cplusplus +extern "C" { +#endif + +#define NRF_EPERM 1 /**< Operation not permitted */ +#define NRF_ENOENT 2 /**< No such file or directory */ +#define NRF_EIO 5 /**< Input/output error */ +#define NRF_ENOEXEC 8 /**< Exec format error */ +#define NRF_EBADF 9 /**< Bad file descriptor */ +#define NRF_ENOMEM 12 /**< Cannot allocate memory */ +#define NRF_EACCES 13 /**< Permission denied */ +#define NRF_EFAULT 14 /**< Bad address */ +#define NRF_ENODEV 19 /**< No such device */ +#define NRF_EINVAL 22 /**< Invalid argument */ +#define NRF_EMFILE 24 /**< Too many open files */ +#define NRF_ENOSPC 28 /**< No space left on device */ +#define NRF_EAGAIN 35 /**< Resource temporarily unavailable*/ +#define NRF_EDOM 37 /**< Domain error */ +#define NRF_EMSGSIZE 40 /**< Message too long */ +#define NRF_EPROTOTYPE 41 /**< Protocol wrong type for socket */ +#define NRF_ENOPROTOOPT 42 /**< Protocol not available */ +#define NRF_EPROTONOSUPPORT 43 /**< Protocol not supported */ +#define NRF_ESOCKTNOSUPPORT 44 /**< Socket type not supported */ +#define NRF_EOPNOTSUPP 45 /**< Operation not supported */ +#define NRF_EAFNOSUPPORT 47 /**< Address family not supported by protocol */ +#define NRF_EADDRINUSE 48 /**< Address already in use */ +#define NRF_ENETDOWN 50 /**< Network is down */ +#define NRF_ENETUNREACH 51 /**< Network is unreachable */ +#define NRF_ENETRESET 52 /**< Connection aborted by network */ +#define NRF_ECONNRESET 54 /**< Connection reset by peer */ +#define NRF_EISCONN 56 /**< Transport endpoint is already connected */ +#define NRF_ENOTCONN 57 /**< Transport endpoint is not connected */ +#define NRF_ETIMEDOUT 60 /**< Connection timed out */ +#define NRF_EBADMSG 77 /**< Bad message */ +#define NRF_ENOBUFS 105 /**< No buffer space available */ + +#define NRF_EHOSTDOWN 112 /**< Host is down */ +#define NRF_EALREADY 114 /**< Operation already in progress */ +#define NRF_EINPROGRESS 115 /**< Operation in progress */ +#define NRF_ECANCELED 125 /**< Operation canceled */ + +#define NRF_ENOKEY 126 /**< Required key not available */ +#define NRF_EKEYEXPIRED 127 /**< Key has expired */ +#define NRF_EKEYREVOKED 128 /**< Key has been revoked */ +#define NRF_EKEYREJECTED 129 /**< Key was rejected by service */ + +#ifdef __cplusplus +} +#endif + +/** + @} +*/ + +#endif // NRF_NFC_ERRNO_H__ diff --git a/components/nrf-nfc/vendor/lib/libnfc_t4t.a b/components/nrf-nfc/vendor/lib/libnfc_t4t.a new file mode 100644 index 0000000..8e4be36 Binary files /dev/null and b/components/nrf-nfc/vendor/lib/libnfc_t4t.a differ diff --git a/components/nrf-nfc/vendor/lib/libnrfx_nfct.a b/components/nrf-nfc/vendor/lib/libnrfx_nfct.a new file mode 100644 index 0000000..7cc27ba Binary files /dev/null and b/components/nrf-nfc/vendor/lib/libnrfx_nfct.a differ diff --git a/components/nrf-nfc/vendor/nfc_t4t_bindings.rs b/components/nrf-nfc/vendor/nfc_t4t_bindings.rs new file mode 100644 index 0000000..18d08cc --- /dev/null +++ b/components/nrf-nfc/vendor/nfc_t4t_bindings.rs @@ -0,0 +1,91 @@ +/* automatically generated by rust-bindgen 0.72.1 */ + +pub const NFC_T4T_MAX_PAYLOAD_SIZE: u32 = 65520; +#[doc = "< Emulated NDEF AID and EF-Files."] +pub const nfc_t4t_emu_mode_t_NFC_T4T_EMUMODE_NDEF: nfc_t4t_emu_mode_t = 0; +#[doc = "< Run just ISO-DEP, deliver I-Frames up."] +pub const nfc_t4t_emu_mode_t_NFC_T4T_EMUMODE_PICC: nfc_t4t_emu_mode_t = 1; +pub type nfc_t4t_emu_mode_t = cty::c_uint; +pub const nfc_t4t_event_t_NFC_T4T_EVENT_NONE: nfc_t4t_event_t = 0; +pub const nfc_t4t_event_t_NFC_T4T_EVENT_FIELD_ON: nfc_t4t_event_t = 1; +pub const nfc_t4t_event_t_NFC_T4T_EVENT_FIELD_OFF: nfc_t4t_event_t = 2; +pub const nfc_t4t_event_t_NFC_T4T_EVENT_NDEF_READ: nfc_t4t_event_t = 3; +pub const nfc_t4t_event_t_NFC_T4T_EVENT_NDEF_UPDATED: nfc_t4t_event_t = 4; +pub const nfc_t4t_event_t_NFC_T4T_EVENT_DATA_TRANSMITTED: nfc_t4t_event_t = 5; +pub const nfc_t4t_event_t_NFC_T4T_EVENT_DATA_IND: nfc_t4t_event_t = 6; +pub type nfc_t4t_event_t = cty::c_uint; +#[doc = "< Dummy value."] +pub const nfc_t4t_data_ind_flags_t_NFC_T4T_DI_FLAG_NONE: nfc_t4t_data_ind_flags_t = 0; +pub const nfc_t4t_data_ind_flags_t_NFC_T4T_DI_FLAG_MORE: nfc_t4t_data_ind_flags_t = 1; +pub type nfc_t4t_data_ind_flags_t = cty::c_uint; +#[doc = "< Value specified by the EMV specification."] +pub const nfc_t4t_fwi_max_val_t_NFC_T4T_FWI_MAX_VAL_EMV: nfc_t4t_fwi_max_val_t = 7; +#[doc = "< Value specified by the NFC specification."] +pub const nfc_t4t_fwi_max_val_t_NFC_T4T_FWI_MAX_VAL_NFC: nfc_t4t_fwi_max_val_t = 8; +pub type nfc_t4t_fwi_max_val_t = cty::c_uint; +pub const nfc_t4t_param_id_t_NFC_T4T_PARAM_FWI: nfc_t4t_param_id_t = 0; +pub const nfc_t4t_param_id_t_NFC_T4T_PARAM_FDT_MIN: nfc_t4t_param_id_t = 1; +pub const nfc_t4t_param_id_t_NFC_T4T_PARAM_SELRES: nfc_t4t_param_id_t = 2; +pub const nfc_t4t_param_id_t_NFC_T4T_PARAM_NFCID1: nfc_t4t_param_id_t = 3; +pub const nfc_t4t_param_id_t_NFC_T4T_PARAM_FWI_MAX: nfc_t4t_param_id_t = 4; +pub type nfc_t4t_param_id_t = cty::c_uint; +#[doc = " @brief Callback to pass events from NFCLib to application.\n\n @param context Application context for callback execution.\n @param event The event that occurred. see nfc_t4t_event.\n @param data Data to send to the application (event specific).\n @param data_length Length of the data. In case of @ref\n NFC_T4T_EVENT_NDEF_UPDATED, this parameter contains the value of the 'NLEN'\n field of the NDEF File; if the value is non-zero, it corresponds to the new\n size of the NDEF Message in the updated NDEF File.\n @param flags Some events deliver flags. see nfc_t4t_event for details."] +pub type nfc_t4t_callback_t = ::core::option::Option< + unsafe extern "C" fn( + context: *mut cty::c_void, + event: nfc_t4t_event_t, + data: *const u8, + data_length: usize, + flags: u32, + ), +>; +unsafe extern "C" { + #[doc = " @brief Register the application callback for event signaling.\n\n The callback will be called by NFCLib to notify the application of relevant\n events. It will be called from the HAL_NFC callback context. The library\n support 3 different Modes of Emulation:\n - Raw ISO-Dep exchanges. All PDUs are signaled through the callback.\n - Read-Only T4T NDEF-Tag. A static buffer is served. Only Field-Status\n callbacks.\n - Read-Write T4T NDEF-Tag. A mutable buffer is used. Only Field-Status\n callbacks.\n\n The default mode is Raw ISO-Dep mode. The two other NDEF T4T modes are\n activated through the corresponding @ref nfc_t4t_ndef_rwpayload_set/ @ref\n nfc_t4t_ndef_staticpayload_set functions. The mode is locked in with a call\n to @ref nfc_t4t_emulation_start.\n\n @param callback Function pointer to the callback.\n @param context Pointer to a memory area used by the callback for execution\n (optional).\n\n @retval 0 Success.\n @retval -NRF_EINVAL Invalid argument (e.g. wrong data length, NULL pointer))\n @retval -NRF_EOPNOTSUPP If emulation is in running state."] + pub fn nfc_t4t_setup(callback: nfc_t4t_callback_t, context: *mut cty::c_void) -> cty::c_int; +} +unsafe extern "C" { + #[doc = " @brief Set emulation buffer and content for a NDEF Tag emulation that is\n Read/Writable.\n\n The buffer needs to be kept accessible for the lifetime of the emulation.\n If an external Reader-Writer changes the NDEF content it is signaled through\n the app-callback. Buffer can be changed during the lifetime of the emulation,\n when NDEF READ or UPDATE procedure is pending, and it will be changed after\n this procedure has completed. To perform this procedure safely, make sure to\n disable NFC interrupts.\n\n @param emulation_buffer Buffer pointer\n @param buffer_length Length of buffer (maximum writable NDEF size)\n\n @retval 0 Success.\n @retval -NRF_EINVAL Invalid argument (for example, wrong data length, NULL pointer).\n @retval -NRF_EOPNOTSUPP If the new buffer has a different length than the first one.\n @retval -NRF_EFAULT If the provided buffer is the currently used buffer."] + pub fn nfc_t4t_ndef_rwpayload_set( + emulation_buffer: *mut u8, + buffer_length: usize, + ) -> cty::c_int; +} +unsafe extern "C" { + #[doc = " @brief Set emulation buffer and content for an NDEF Tag emulation that is\n Read-Only.\n\n The buffer needs to be kept accessible for the lifetime of the emulation.\n Since no write access is done to the buffer, the content could reside in\n flash memory.\n\n @param emulation_buffer Const buffer pointer\n @param buffer_length Length of contained NDEF payload message\n\n @retval 0 Success.\n @retval -NRF_EINVAL Invalid argument (for example, wrong data length, NULL pointer).\n @retval -NRF_EOPNOTSUPP Emulation is in running stated."] + pub fn nfc_t4t_ndef_staticpayload_set( + emulation_buffer: *const u8, + buffer_length: usize, + ) -> cty::c_int; +} +unsafe extern "C" { + #[doc = " @brief Send a raw response PDU after getting a Request PDU callback.\n\n When the library works in raw ISO-DEP mode it will signal request PDUs\n through the callback. The application then has to answer with a response PDU.\n It will use this function to send back the response PDU. This function can\n not be used in T4T NDEF (RW / STATIC) emulation modes.\n\n The lower ISODEP layer will handle the defragmentation of a long response PDU\n into smalleR pieces that the PCD can understand.\n\n @param pdu Const PDU pointer.\n @param pdu_length Length of PDU.\n\n @retval 0 Success.\n @retval -NRF_EINVAL Invalid argument (for example, wrong data length, NULL pointer).\n @retval -NRF_EOPNOTSUPP Emulation is in running state."] + pub fn nfc_t4t_response_pdu_send(pdu: *const u8, pdu_length: usize) -> cty::c_int; +} +unsafe extern "C" { + #[doc = " @brief Set an NFC parameter.\n\n Allows to set an NFC configuration parameter.\n\n @param id ID of the parameter to set.\n @param data Pointer to a buffer containing the data to set.\n @param data_length Size of the buffer containing the data to set.\n\n @retval 0 Success.\n @retval -NRF_EINVAL Invalid argument (for example, wrong data length, NULL pointer)."] + pub fn nfc_t4t_parameter_set( + id: nfc_t4t_param_id_t, + data: *mut cty::c_void, + data_length: usize, + ) -> cty::c_int; +} +unsafe extern "C" { + #[doc = " @brief Query an NFC parameter value.\n\n The queried value will be placed into the passed data buffer.\n If the buffer is too small, max_data_length will contain the required\n buffer size. If the buffer is big enough, max_data_length will contain\n the actual size of the data.\n\n @param id ID of the parameter to query.\n @param data Pointer to a buffer receiving the queried data.\n @param max_data_length Size of the buffer, receives actual size of queried\n data.\n\n @retval 0 Success.\n @retval -NRF_EINVAL Invalid argument (for example, wrong data length, NULL pointer)."] + pub fn nfc_t4t_parameter_get( + id: nfc_t4t_param_id_t, + data: *mut cty::c_void, + max_data_length: *mut usize, + ) -> cty::c_int; +} +unsafe extern "C" { + #[doc = " @brief Activate the NFC frontend.\n\n Only after calling this function, events will be posted to the application\n callback.\n\n @retval 0 Success.\n @retval -NRF_EOPNOTSUPP Already started."] + pub fn nfc_t4t_emulation_start() -> cty::c_int; +} +unsafe extern "C" { + #[doc = " @brief Deactivate the NFC frontend.\n\n After calling this function, no more events will be posted to the application\n callback.\n\n @retval 0 Success.\n @retval -NRF_EOPNOTSUPP Emulation was already stopped."] + pub fn nfc_t4t_emulation_stop() -> cty::c_int; +} +unsafe extern "C" { + #[doc = " @brief Release reference to application callback.\n\n After calling this function, the passed callback pointer is no longer\n considered valid.\n After calling this function, the passed ndef pointer is no longer\n considered valid.\n\n You need to restart with @ref nfc_t4t_setup to run a new Emulation.\n\n @retval 0 Success.\n @retval -NRF_EOPNOTSUPP The NFC T4T has been de-initialized already."] + pub fn nfc_t4t_done() -> cty::c_int; +} diff --git a/components/nrf-nfc/vendor/nrfx_nfct_bindings.rs b/components/nrf-nfc/vendor/nrfx_nfct_bindings.rs new file mode 100644 index 0000000..b70e602 --- /dev/null +++ b/components/nrf-nfc/vendor/nrfx_nfct_bindings.rs @@ -0,0 +1,552 @@ +/* automatically generated by rust-bindgen 0.72.1 */ + +pub const NRFX_NFCT_ENABLED: u32 = 1; +pub const NRFX_NFCT_CONFIG_TIMER_INSTANCE_ID: u32 = 4; +pub const NRFX_NFCT_CONFIG_IRQ_PRIORITY: u32 = 6; +pub const NRFX_NFCT_CONFIG_LOG_ENABLED: u32 = 0; +pub const NRF_NFCT_BASE: u32 = 1073762304; +pub const NRF_NFCT_CRC_SIZE: u32 = 2; +pub const NRF_NFCT_FAME_DELAY_MAX_DEFAULT: u32 = 4096; +pub const NRF_NFCT_FAME_DELAY_MIN_DEFAULT: u32 = 1152; +pub const NRF_NFCT_FRAME_DELAY_MAX_MAX_VALUE: u32 = 1048575; +pub const NRF_NFCT_HAS_STOPTX_TASK: u32 = 0; +pub const NRF_NFCT_HAS_MODULATION_PSEL_REG: u32 = 0; +pub const NRF_NFCT_HAS_MODULATION_CTRL_REG: u32 = 0; +pub const NRF_NFCT_HAS_TAG_STATE_REG: u32 = 1; +pub const NRF_NFCT_HAS_SLEEP_STATE_REG: u32 = 1; +pub const NRF_NFCT_HAS_AUTOCOLRES_CONFIG_REG: u32 = 1; +pub const NRF_NFCT_HAS_PAD_CONFIG_REG: u32 = 0; +pub const NRF_NFCT_HAS_BIAS_CONFIG_TRIM_REG: u32 = 0; +pub const NRF_NFCT_HAS_ERROR_FIELD_TOO_STRONG: u32 = 0; +pub const NRF_NFCT_HAS_ERROR_FIELD_TOO_WEAK: u32 = 0; +pub const NRFX_NFCT_NFCID1_SINGLE_SIZE: u32 = 4; +pub const NRFX_NFCT_NFCID1_DOUBLE_SIZE: u32 = 7; +pub const NRFX_NFCT_NFCID1_TRIPLE_SIZE: u32 = 10; +pub const NRFX_NFCT_NFCID1_DEFAULT_LEN: u32 = 7; +#[doc = "< Activate the NFCT peripheral for the incoming and outgoing frames, change state to activated."] +pub const nrf_nfct_task_t_NRF_NFCT_TASK_ACTIVATE: nrf_nfct_task_t = 0; +#[doc = "< Disable the NFCT peripheral."] +pub const nrf_nfct_task_t_NRF_NFCT_TASK_DISABLE: nrf_nfct_task_t = 4; +#[doc = "< Enable the NFC sense field mode, change state to sense mode."] +pub const nrf_nfct_task_t_NRF_NFCT_TASK_SENSE: nrf_nfct_task_t = 8; +#[doc = "< Start the transmission of an outgoing frame, change state to transmit."] +pub const nrf_nfct_task_t_NRF_NFCT_TASK_STARTTX: nrf_nfct_task_t = 12; +#[doc = "< Initialize EasyDMA for receive."] +pub const nrf_nfct_task_t_NRF_NFCT_TASK_ENABLERXDATA: nrf_nfct_task_t = 28; +#[doc = "< Force state machine to the IDLE state."] +pub const nrf_nfct_task_t_NRF_NFCT_TASK_GOIDLE: nrf_nfct_task_t = 36; +#[doc = "< Force state machine to the SLEEP_A state."] +pub const nrf_nfct_task_t_NRF_NFCT_TASK_GOSLEEP: nrf_nfct_task_t = 40; +#[doc = " @brief NFCT tasks."] +pub type nrf_nfct_task_t = cty::c_uint; +#[doc = "< The NFCT peripheral is ready to receive and send frames."] +pub const nrf_nfct_event_t_NRF_NFCT_EVENT_READY: nrf_nfct_event_t = 256; +#[doc = "< Remote NFC field is detected."] +pub const nrf_nfct_event_t_NRF_NFCT_EVENT_FIELDDETECTED: nrf_nfct_event_t = 260; +#[doc = "< Remote NFC field is lost."] +pub const nrf_nfct_event_t_NRF_NFCT_EVENT_FIELDLOST: nrf_nfct_event_t = 264; +#[doc = "< The start of the first symbol of a transmitted frame."] +pub const nrf_nfct_event_t_NRF_NFCT_EVENT_TXFRAMESTART: nrf_nfct_event_t = 268; +#[doc = "< The end of the last transmitted on-air symbol of a frame."] +pub const nrf_nfct_event_t_NRF_NFCT_EVENT_TXFRAMEEND: nrf_nfct_event_t = 272; +#[doc = "< The end of the first symbol of a received frame."] +pub const nrf_nfct_event_t_NRF_NFCT_EVENT_RXFRAMESTART: nrf_nfct_event_t = 276; +#[doc = "< Received data was checked (CRC, parity) and transferred to RAM, and EasyDMA ended accessing the RX buffer."] +pub const nrf_nfct_event_t_NRF_NFCT_EVENT_RXFRAMEEND: nrf_nfct_event_t = 280; +#[doc = "< NFC error reported. The ERRORSTATUS register contains details on the source of the error."] +pub const nrf_nfct_event_t_NRF_NFCT_EVENT_ERROR: nrf_nfct_event_t = 284; +#[doc = "< NFC RX frame error reported. The FRAMESTATUS.RX register contains details on the source of the error."] +pub const nrf_nfct_event_t_NRF_NFCT_EVENT_RXERROR: nrf_nfct_event_t = 296; +#[doc = "< RX buffer (as defined by PACKETPTR and MAXLEN) in Data RAM full."] +pub const nrf_nfct_event_t_NRF_NFCT_EVENT_ENDRX: nrf_nfct_event_t = 300; +#[doc = "< Transmission of data in RAM ended, and EasyDMA ended accessing the TX buffer."] +pub const nrf_nfct_event_t_NRF_NFCT_EVENT_ENDTX: nrf_nfct_event_t = 304; +#[doc = "< Auto collision resolution process started."] +pub const nrf_nfct_event_t_NRF_NFCT_EVENT_AUTOCOLRESSTARTED: nrf_nfct_event_t = 312; +#[doc = "< NFC auto collision resolution error reported."] +pub const nrf_nfct_event_t_NRF_NFCT_EVENT_COLLISION: nrf_nfct_event_t = 328; +#[doc = "< NFC auto collision resolution successfully completed."] +pub const nrf_nfct_event_t_NRF_NFCT_EVENT_SELECTED: nrf_nfct_event_t = 332; +#[doc = "< EasyDMA is ready to receive or send frames."] +pub const nrf_nfct_event_t_NRF_NFCT_EVENT_STARTED: nrf_nfct_event_t = 336; +#[doc = " @brief NFCT events."] +pub type nrf_nfct_event_t = cty::c_uint; +#[doc = "< Shortcut between the FIELDDETECTED event and the ACTIVATE task."] +pub const nrf_nfct_short_mask_t_NRF_NFCT_SHORT_FIELDDETECTED_ACTIVATE_MASK: nrf_nfct_short_mask_t = + 1; +#[doc = "< Shortcut between the FIELDLOST event and the SENSE task."] +pub const nrf_nfct_short_mask_t_NRF_NFCT_SHORT_FIELDLOST_SENSE_MASK: nrf_nfct_short_mask_t = 2; +#[doc = "< Shortcut between the TXFRAMEEND event and the ENABLERXDATA task."] +pub const nrf_nfct_short_mask_t_NRF_NFCT_SHORT_TXFRAMEEND_ENABLERXDATA_MASK: nrf_nfct_short_mask_t = + 32; +#[doc = " @brief NFCT shortcuts."] +pub type nrf_nfct_short_mask_t = cty::c_uint; +#[doc = "< Interrupt on READY event."] +pub const nrf_nfct_int_mask_t_NRF_NFCT_INT_READY_MASK: nrf_nfct_int_mask_t = 1; +#[doc = "< Interrupt on FIELDDETECTED event."] +pub const nrf_nfct_int_mask_t_NRF_NFCT_INT_FIELDDETECTED_MASK: nrf_nfct_int_mask_t = 2; +#[doc = "< Interrupt on FIELDLOST event."] +pub const nrf_nfct_int_mask_t_NRF_NFCT_INT_FIELDLOST_MASK: nrf_nfct_int_mask_t = 4; +#[doc = "< Interrupt on TXFRAMESTART event."] +pub const nrf_nfct_int_mask_t_NRF_NFCT_INT_TXFRAMESTART_MASK: nrf_nfct_int_mask_t = 8; +#[doc = "< Interrupt on TXFRAMEEND event."] +pub const nrf_nfct_int_mask_t_NRF_NFCT_INT_TXFRAMEEND_MASK: nrf_nfct_int_mask_t = 16; +#[doc = "< Interrupt on RXFRAMESTART event."] +pub const nrf_nfct_int_mask_t_NRF_NFCT_INT_RXFRAMESTART_MASK: nrf_nfct_int_mask_t = 32; +#[doc = "< Interrupt on RXFRAMEEND event."] +pub const nrf_nfct_int_mask_t_NRF_NFCT_INT_RXFRAMEEND_MASK: nrf_nfct_int_mask_t = 64; +#[doc = "< Interrupt on ERROR event."] +pub const nrf_nfct_int_mask_t_NRF_NFCT_INT_ERROR_MASK: nrf_nfct_int_mask_t = 128; +#[doc = "< Interrupt on RXERROR event."] +pub const nrf_nfct_int_mask_t_NRF_NFCT_INT_RXERROR_MASK: nrf_nfct_int_mask_t = 1024; +#[doc = "< Interrupt on ENDRX event."] +pub const nrf_nfct_int_mask_t_NRF_NFCT_INT_ENDRX_MASK: nrf_nfct_int_mask_t = 2048; +#[doc = "< Interrupt on ENDTX event."] +pub const nrf_nfct_int_mask_t_NRF_NFCT_INT_ENDTX_MASK: nrf_nfct_int_mask_t = 4096; +#[doc = "< Interrupt on AUTOCOLRESSTARTED event."] +pub const nrf_nfct_int_mask_t_NRF_NFCT_INT_AUTOCOLRESSTARTED_MASK: nrf_nfct_int_mask_t = 16384; +#[doc = "< Interrupt on COLLISION event."] +pub const nrf_nfct_int_mask_t_NRF_NFCT_INT_COLLISION_MASK: nrf_nfct_int_mask_t = 262144; +#[doc = "< Interrupt on SELECTED event."] +pub const nrf_nfct_int_mask_t_NRF_NFCT_INT_SELECTED_MASK: nrf_nfct_int_mask_t = 524288; +#[doc = "< Interrupt on STARTED event."] +pub const nrf_nfct_int_mask_t_NRF_NFCT_INT_STARTED_MASK: nrf_nfct_int_mask_t = 1048576; +#[doc = " @brief NFCT interrupts."] +pub type nrf_nfct_int_mask_t = cty::c_uint; +#[doc = "< Timeout of the Frame Delay Timer (no frame transmission started in the FDT window)."] +pub const nrf_nfct_error_status_t_NRF_NFCT_ERROR_FRAMEDELAYTIMEOUT_MASK: nrf_nfct_error_status_t = + 1; +#[doc = " @brief NFC error status bit masks."] +pub type nrf_nfct_error_status_t = cty::c_uint; +#[doc = "< CRC status mask."] +pub const nrf_nfct_rx_frame_status_t_NRF_NFCT_RX_FRAME_STATUS_CRC_MASK: nrf_nfct_rx_frame_status_t = + 1; +#[doc = "< Parity status mask."] +pub const nrf_nfct_rx_frame_status_t_NRF_NFCT_RX_FRAME_STATUS_PARITY_MASK: + nrf_nfct_rx_frame_status_t = 4; +#[doc = "< Overrun status mask."] +pub const nrf_nfct_rx_frame_status_t_NRF_NFCT_RX_FRAME_STATUS_OVERRUN_MASK: + nrf_nfct_rx_frame_status_t = 8; +#[doc = " @brief NFC received frame status bit masks."] +pub type nrf_nfct_rx_frame_status_t = cty::c_uint; +#[doc = "< Disabled or sensing NFC field."] +pub const nrf_nfct_tag_state_t_NRF_NFCT_TAG_STATE_DISABLED: nrf_nfct_tag_state_t = 0; +#[doc = "< Ramping up."] +pub const nrf_nfct_tag_state_t_NRF_NFCT_TAG_STATE_RAMP_UP: nrf_nfct_tag_state_t = 2; +#[doc = "< Idle."] +pub const nrf_nfct_tag_state_t_NRF_NFCT_TAG_STATE_IDLE: nrf_nfct_tag_state_t = 3; +#[doc = "< Receiving data."] +pub const nrf_nfct_tag_state_t_NRF_NFCT_TAG_STATE_RECEIVE: nrf_nfct_tag_state_t = 4; +#[doc = "< Counting Frame Delay Time since the last symbol of the last received frame."] +pub const nrf_nfct_tag_state_t_NRF_NFCT_TAG_STATE_FRAME_DELAY: nrf_nfct_tag_state_t = 5; +#[doc = "< Transmitting data."] +pub const nrf_nfct_tag_state_t_NRF_NFCT_TAG_STATE_TRANSMIT: nrf_nfct_tag_state_t = 6; +#[doc = " @brief NFC tag state."] +pub type nrf_nfct_tag_state_t = cty::c_uint; +#[doc = "< 'IDLE' state."] +pub const nrf_nfct_sleep_state_t_NRF_NFCT_SLEEP_STATE_IDLE: nrf_nfct_sleep_state_t = 0; +#[doc = "< 'SLEEP_A' state."] +pub const nrf_nfct_sleep_state_t_NRF_NFCT_SLEEP_STATE_SLEEP_A: nrf_nfct_sleep_state_t = 1; +#[doc = " @brief NFC tag sleep state.\n\n @details Shows the sleep state during automatic collision resolution\n according to the NFC Forum Activity Technical Specification v2.0."] +pub type nrf_nfct_sleep_state_t = cty::c_uint; +#[doc = "< Field presence mask."] +pub const nrf_nfct_field_state_t_NRF_NFCT_FIELD_STATE_PRESENT_MASK: nrf_nfct_field_state_t = 1; +#[doc = "< Field lock mask."] +pub const nrf_nfct_field_state_t_NRF_NFCT_FIELD_STATE_LOCK_MASK: nrf_nfct_field_state_t = 2; +#[doc = " @brief NFC field state bit masks."] +pub type nrf_nfct_field_state_t = cty::c_uint; +#[doc = "< Frame transmission starts when @ref NRF_NFCT_TASK_STARTTX is set (delay timer is not used)."] +pub const nrf_nfct_frame_delay_mode_t_NRF_NFCT_FRAME_DELAY_MODE_FREERUN: + nrf_nfct_frame_delay_mode_t = 0; +#[doc = "< Frame transmission starts in a window between FRAMEDELAYMIN and FRAMEDELAYMAX."] +pub const nrf_nfct_frame_delay_mode_t_NRF_NFCT_FRAME_DELAY_MODE_WINDOW: + nrf_nfct_frame_delay_mode_t = 1; +#[doc = "< Frame transmission starts when the delay timer reaches FRAMEDELAYMAX."] +pub const nrf_nfct_frame_delay_mode_t_NRF_NFCT_FRAME_DELAY_MODE_EXACTVAL: + nrf_nfct_frame_delay_mode_t = 2; +#[doc = "< Frame transmission starts in a bit grid between FRAMEDELAYMIN and FRAMEDELAYMAX."] +pub const nrf_nfct_frame_delay_mode_t_NRF_NFCT_FRAME_DELAY_MODE_WINDOWGRID: + nrf_nfct_frame_delay_mode_t = 3; +#[doc = " @brief NFC frame delay mode for data transmission."] +pub type nrf_nfct_frame_delay_mode_t = cty::c_uint; +#[doc = "< Indicates whether parity is added in the transmitted frames."] +pub const nrf_nfct_tx_frame_config_t_NRF_NFCT_TX_FRAME_CONFIG_PARITY: nrf_nfct_tx_frame_config_t = + 1; +#[doc = "< Indicates whether unused bits are discarded at the start or at the end of the transmitted frames."] +pub const nrf_nfct_tx_frame_config_t_NRF_NFCT_TX_FRAME_CONFIG_DISCARD_START: + nrf_nfct_tx_frame_config_t = 2; +#[doc = "< Indicates whether SoF symbol is added in the transmitted frames."] +pub const nrf_nfct_tx_frame_config_t_NRF_NFCT_TX_FRAME_CONFIG_SOF: nrf_nfct_tx_frame_config_t = 4; +#[doc = "< Indicates whether CRC is added in the transmitted frames."] +pub const nrf_nfct_tx_frame_config_t_NRF_NFCT_TX_FRAME_CONFIG_CRC16: nrf_nfct_tx_frame_config_t = + 16; +#[doc = " @brief Bit masks for NFC transmission frame configuration."] +pub type nrf_nfct_tx_frame_config_t = cty::c_uint; +#[doc = "< Indicates whether parity is expected in the received frames."] +pub const nrf_nfct_rx_frame_config_t_NRF_NFCT_RX_FRAME_CONFIG_PARITY: nrf_nfct_rx_frame_config_t = + 1; +#[doc = "< Indicates whether SoF symbol is expected in the received frames."] +pub const nrf_nfct_rx_frame_config_t_NRF_NFCT_RX_FRAME_CONFIG_SOF: nrf_nfct_rx_frame_config_t = 4; +#[doc = "< Indicates whether CRC is expected and checked in the received frames."] +pub const nrf_nfct_rx_frame_config_t_NRF_NFCT_RX_FRAME_CONFIG_CRC16: nrf_nfct_rx_frame_config_t = + 16; +#[doc = " @brief Bit masks for NFC reception frame configuration."] +pub type nrf_nfct_rx_frame_config_t = cty::c_uint; +pub const nrf_nfct_sensres_nfcid1_size_t_NRF_NFCT_SENSRES_NFCID1_SIZE_SINGLE: + nrf_nfct_sensres_nfcid1_size_t = 0; +pub const nrf_nfct_sensres_nfcid1_size_t_NRF_NFCT_SENSRES_NFCID1_SIZE_DOUBLE: + nrf_nfct_sensres_nfcid1_size_t = 64; +pub const nrf_nfct_sensres_nfcid1_size_t_NRF_NFCT_SENSRES_NFCID1_SIZE_TRIPLE: + nrf_nfct_sensres_nfcid1_size_t = 128; +pub const nrf_nfct_sensres_nfcid1_size_t_NRF_NFCT_SENSRES_NFCID1_SIZE_DEFAULT: + nrf_nfct_sensres_nfcid1_size_t = 192; +#[doc = " @brief 'NFCI1 size' NFC field configuration for the SENS_RES frame according to the NFC Forum\n Digital Protocol Technical Specification."] +pub type nrf_nfct_sensres_nfcid1_size_t = cty::c_uint; +#[doc = " @brief Bias trim configuration."] +#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] +pub struct nrf_nfct_bias_config_t { + #[doc = "< Fine trim IBPSR 4 µA bias current."] + pub trim_ibpsr: u8, + #[doc = "< Coarse trim IBPSR 4 µA."] + pub coarse_ibpsr: u8, + #[doc = "< Reference voltage level."] + pub reference_volatge: u8, + #[doc = "< Spare."] + pub spare: u8, +} +pub const nrf_nfct_sensres_bit_frame_sdd_t_NRF_NFCT_SENSRES_BIT_FRAME_SDD_00000: + nrf_nfct_sensres_bit_frame_sdd_t = 0; +pub const nrf_nfct_sensres_bit_frame_sdd_t_NRF_NFCT_SENSRES_BIT_FRAME_SDD_00001: + nrf_nfct_sensres_bit_frame_sdd_t = 1; +pub const nrf_nfct_sensres_bit_frame_sdd_t_NRF_NFCT_SENSRES_BIT_FRAME_SDD_00010: + nrf_nfct_sensres_bit_frame_sdd_t = 2; +pub const nrf_nfct_sensres_bit_frame_sdd_t_NRF_NFCT_SENSRES_BIT_FRAME_SDD_00100: + nrf_nfct_sensres_bit_frame_sdd_t = 4; +pub const nrf_nfct_sensres_bit_frame_sdd_t_NRF_NFCT_SENSRES_BIT_FRAME_SDD_01000: + nrf_nfct_sensres_bit_frame_sdd_t = 8; +pub const nrf_nfct_sensres_bit_frame_sdd_t_NRF_NFCT_SENSRES_BIT_FRAME_SDD_10000: + nrf_nfct_sensres_bit_frame_sdd_t = 16; +#[doc = " @brief 'Bit frame SDD' NFC field configuration for the SENS_RES frame according to the NFC\n Forum Digital Protocol Technical Specification."] +pub type nrf_nfct_sensres_bit_frame_sdd_t = cty::c_uint; +pub const nrf_nfct_sensres_platform_config_t_NRF_NFCT_SENSRES_PLATFORM_CONFIG_T1T: + nrf_nfct_sensres_platform_config_t = 1536; +pub const nrf_nfct_sensres_platform_config_t_NRF_NFCT_SENSRES_PLATFORM_CONFIG_OTHER: + nrf_nfct_sensres_platform_config_t = 0; +#[doc = " @brief 'Platofrm Config' NFC field configuration for the SENS_RES frame according to the NFC\n Forum Digital Protocol Technical Specification."] +pub type nrf_nfct_sensres_platform_config_t = cty::c_uint; +#[doc = "< Type 2 Tag platform."] +pub const nrf_nfct_selres_protocol_t_NRF_NFCT_SELRES_PROTOCOL_T2T: nrf_nfct_selres_protocol_t = 0; +#[doc = "< Type 4A Tag platform."] +pub const nrf_nfct_selres_protocol_t_NRF_NFCT_SELRES_PROTOCOL_T4AT: nrf_nfct_selres_protocol_t = 1; +#[doc = "< NFC-DEP Protocol."] +pub const nrf_nfct_selres_protocol_t_NRF_NFCT_SELRES_PROTOCOL_NFCDEP: nrf_nfct_selres_protocol_t = + 2; +#[doc = "< NFC-DEP Protocol and Type 4A Tag platform)."] +pub const nrf_nfct_selres_protocol_t_NRF_NFCT_SELRES_PROTOCOL_NFCDEP_T4AT: + nrf_nfct_selres_protocol_t = 3; +#[doc = " @brief Protocol NFC field (bits b7 and b6) configuration for the SEL_RES frame according to\n the NFC Forum Digital Protocol Technical Specification."] +pub type nrf_nfct_selres_protocol_t = cty::c_uint; +#[doc = "< NFC Tag is disabled (no sensing of an external NFC field)."] +pub const nrfx_nfct_state_t_NRFX_NFCT_STATE_DISABLED: nrfx_nfct_state_t = 4; +#[doc = "< NFC Tag is sensing whether there is an external NFC field."] +pub const nrfx_nfct_state_t_NRFX_NFCT_STATE_SENSING: nrfx_nfct_state_t = 8; +#[doc = "< NFC Tag is powered-up (see @ref nrfx_nfct_active_state_t for possible substates)."] +pub const nrfx_nfct_state_t_NRFX_NFCT_STATE_ACTIVATED: nrfx_nfct_state_t = 0; +#[doc = " @brief NFCT hardware states."] +pub type nrfx_nfct_state_t = cty::c_uint; +#[doc = "< NFC Tag is activated and idle (not selected by a reader)."] +pub const nrfx_nfct_active_state_t_NRFX_NFCT_ACTIVE_STATE_IDLE: nrfx_nfct_active_state_t = 36; +#[doc = "< NFC Tag is sleeping."] +pub const nrfx_nfct_active_state_t_NRFX_NFCT_ACTIVE_STATE_SLEEP: nrfx_nfct_active_state_t = 40; +#[doc = "< NFC Tag is either sleeping or idle, depending on the previous state before being selected by a poller."] +pub const nrfx_nfct_active_state_t_NRFX_NFCT_ACTIVE_STATE_DEFAULT: nrfx_nfct_active_state_t = 41; +#[doc = " @brief NFC tag states, when NFCT hardware is activated.\n\n @details These states are substates of the @ref NRFX_NFCT_STATE_ACTIVATED state."] +pub type nrfx_nfct_active_state_t = cty::c_uint; +#[doc = "< External NFC field is detected."] +pub const nrfx_nfct_evt_id_t_NRFX_NFCT_EVT_FIELD_DETECTED: nrfx_nfct_evt_id_t = 2; +#[doc = "< External NFC Field is lost."] +pub const nrfx_nfct_evt_id_t_NRFX_NFCT_EVT_FIELD_LOST: nrfx_nfct_evt_id_t = 4; +#[doc = "< Tag was selected by the poller."] +pub const nrfx_nfct_evt_id_t_NRFX_NFCT_EVT_SELECTED: nrfx_nfct_evt_id_t = 524288; +#[doc = "< Data frame reception started."] +pub const nrfx_nfct_evt_id_t_NRFX_NFCT_EVT_RX_FRAMESTART: nrfx_nfct_evt_id_t = 32; +#[doc = "< Data frame is received."] +pub const nrfx_nfct_evt_id_t_NRFX_NFCT_EVT_RX_FRAMEEND: nrfx_nfct_evt_id_t = 64; +#[doc = "< Data frame transmission started."] +pub const nrfx_nfct_evt_id_t_NRFX_NFCT_EVT_TX_FRAMESTART: nrfx_nfct_evt_id_t = 8; +#[doc = "< Data frame is transmitted."] +pub const nrfx_nfct_evt_id_t_NRFX_NFCT_EVT_TX_FRAMEEND: nrfx_nfct_evt_id_t = 16; +#[doc = "< Error occurred in an NFC communication."] +pub const nrfx_nfct_evt_id_t_NRFX_NFCT_EVT_ERROR: nrfx_nfct_evt_id_t = 128; +#[doc = " @brief NFCT driver event types, passed to the upper-layer callback function\n provided during the initialization."] +pub type nrfx_nfct_evt_id_t = cty::c_uint; +#[doc = "< No response frame was transmitted to the poller in the transmit window."] +pub const nrfx_nfct_error_t_NRFX_NFCT_ERROR_FRAMEDELAYTIMEOUT: nrfx_nfct_error_t = 0; +#[doc = "< Total number of possible errors."] +pub const nrfx_nfct_error_t_NRFX_NFCT_ERROR_NUM: nrfx_nfct_error_t = 1; +#[doc = " @brief NFCT timing-related error types."] +pub type nrfx_nfct_error_t = cty::c_uint; +#[doc = "< NFC-A Frame Delay Time parameter."] +pub const nrfx_nfct_param_id_t_NRFX_NFCT_PARAM_ID_FDT: nrfx_nfct_param_id_t = 0; +#[doc = "< NFC-A Frame Delay Time Min parameter."] +pub const nrfx_nfct_param_id_t_NRFX_NFCT_PARAM_ID_FDT_MIN: nrfx_nfct_param_id_t = 1; +#[doc = "< Value of the 'Protocol' field in the NFC-A SEL_RES frame."] +pub const nrfx_nfct_param_id_t_NRFX_NFCT_PARAM_ID_SEL_RES: nrfx_nfct_param_id_t = 2; +#[doc = "< NFC-A NFCID1 setting (NFC tag identifier)."] +pub const nrfx_nfct_param_id_t_NRFX_NFCT_PARAM_ID_NFCID1: nrfx_nfct_param_id_t = 3; +#[doc = " @brief NFCT driver parameter types."] +pub type nrfx_nfct_param_id_t = cty::c_uint; +#[doc = " @brief NFCID1 descriptor."] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct nrfx_nfct_nfcid1_t { + #[doc = "< NFCID1 data."] + pub p_id: *const u8, + #[doc = "< NFCID1 size."] + pub id_size: u8, +} +impl Default for nrfx_nfct_nfcid1_t { + fn default() -> Self { + let mut s = ::core::mem::MaybeUninit::::uninit(); + unsafe { + ::core::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +#[doc = " @brief NFCT driver parameter descriptor."] +#[repr(C)] +#[derive(Copy, Clone)] +pub struct nrfx_nfct_param_t { + #[doc = "< Type of parameter."] + pub id: nrfx_nfct_param_id_t, + #[doc = "< Union to store parameter data."] + pub data: nrfx_nfct_param_t__bindgen_ty_1, +} +#[repr(C)] +#[derive(Copy, Clone)] +pub union nrfx_nfct_param_t__bindgen_ty_1 { + #[doc = "< NFC-A Frame Delay Time. Filled when nrfx_nfct_param_t.id is @ref NRFX_NFCT_PARAM_ID_FDT."] + pub fdt: u32, + #[doc = "< NFC-A Frame Delay Time Min. Filled when nrfx_nfct_param_t.id is @ref NRFX_NFCT_PARAM_ID_FDT_MIN."] + pub fdt_min: u32, + #[doc = "< NFC-A value of the 'Protocol' field in the SEL_RES frame. Filled when nrfx_nfct_param_t.id is @ref NRFX_NFCT_PARAM_ID_SEL_RES."] + pub sel_res_protocol: u8, + #[doc = "< NFC-A NFCID1 value (tag identifier). Filled when nrfx_nfct_param_t.id is @ref NRFX_NFCT_PARAM_ID_NFCID1."] + pub nfcid1: nrfx_nfct_nfcid1_t, +} +impl Default for nrfx_nfct_param_t__bindgen_ty_1 { + fn default() -> Self { + let mut s = ::core::mem::MaybeUninit::::uninit(); + unsafe { + ::core::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +impl Default for nrfx_nfct_param_t { + fn default() -> Self { + let mut s = ::core::mem::MaybeUninit::::uninit(); + unsafe { + ::core::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +#[doc = " @brief NFCT driver RX/TX buffer descriptor."] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct nrfx_nfct_data_desc_t { + #[doc = "< RX/TX buffer size."] + pub data_size: u32, + #[doc = "< RX/TX buffer."] + pub p_data: *const u8, +} +impl Default for nrfx_nfct_data_desc_t { + fn default() -> Self { + let mut s = ::core::mem::MaybeUninit::::uninit(); + unsafe { + ::core::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +#[doc = " @brief Structure used to describe the @ref NRFX_NFCT_EVT_RX_FRAMEEND event type."] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct nrfx_nfct_evt_rx_frameend_t { + #[doc = "< RX error status."] + pub rx_status: u32, + #[doc = "< RX buffer."] + pub rx_data: nrfx_nfct_data_desc_t, +} +impl Default for nrfx_nfct_evt_rx_frameend_t { + fn default() -> Self { + let mut s = ::core::mem::MaybeUninit::::uninit(); + unsafe { + ::core::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +#[doc = " @brief Structure used to describe the @ref NRFX_NFCT_EVT_TX_FRAMESTART event type."] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct nrfx_nfct_evt_tx_framestart_t { + #[doc = "< TX buffer."] + pub tx_data: nrfx_nfct_data_desc_t, +} +impl Default for nrfx_nfct_evt_tx_framestart_t { + fn default() -> Self { + let mut s = ::core::mem::MaybeUninit::::uninit(); + unsafe { + ::core::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +#[doc = " @brief Structure used to describe the @ref NRFX_NFCT_EVT_ERROR event type."] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct nrfx_nfct_evt_error_t { + #[doc = "< Reason for error."] + pub reason: nrfx_nfct_error_t, +} +impl Default for nrfx_nfct_evt_error_t { + fn default() -> Self { + let mut s = ::core::mem::MaybeUninit::::uninit(); + unsafe { + ::core::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +#[doc = " @brief NFCT driver event."] +#[repr(C)] +#[derive(Copy, Clone)] +pub struct nrfx_nfct_evt_t { + #[doc = "< Type of event."] + pub evt_id: nrfx_nfct_evt_id_t, + #[doc = "< Union to store event data."] + pub params: nrfx_nfct_evt_t__bindgen_ty_1, +} +#[repr(C)] +#[derive(Copy, Clone)] +pub union nrfx_nfct_evt_t__bindgen_ty_1 { + #[doc = "< End of the RX frame data. Filled when nrfx_nfct_evt_t.evt_id is @ref NRFX_NFCT_EVT_RX_FRAMEEND."] + pub rx_frameend: nrfx_nfct_evt_rx_frameend_t, + #[doc = "< Start of the TX frame data. Filled when nrfx_nfct_evt_t.evt_id is @ref NRFX_NFCT_EVT_TX_FRAMESTART."] + pub tx_framestart: nrfx_nfct_evt_tx_framestart_t, + #[doc = "< Error data. Filled when nrfx_nfct_evt_t.evt_id is @ref NRFX_NFCT_EVT_ERROR."] + pub error: nrfx_nfct_evt_error_t, +} +impl Default for nrfx_nfct_evt_t__bindgen_ty_1 { + fn default() -> Self { + let mut s = ::core::mem::MaybeUninit::::uninit(); + unsafe { + ::core::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +impl Default for nrfx_nfct_evt_t { + fn default() -> Self { + let mut s = ::core::mem::MaybeUninit::::uninit(); + unsafe { + ::core::ptr::write_bytes(s.as_mut_ptr(), 0, 1); + s.assume_init() + } + } +} +#[doc = " @brief Callback descriptor to pass events from the NFCT driver to the upper layer.\n\n @param[in] p_event Pointer to the event descriptor.\n\n @note @ref NRFX_NFCT_EVT_FIELD_DETECTED and @ref NRFX_NFCT_EVT_FIELD_LOST are generated only on field state transitions,\n i.e. there will be no multiple events of the same type (out of the 2 mentioned) coming in a row."] +pub type nrfx_nfct_handler_t = + ::core::option::Option; +#[doc = " @brief NFCT driver configuration structure."] +#[repr(C)] +#[derive(Debug, Default, Copy, Clone)] +pub struct nrfx_nfct_config_t { + #[doc = "< Mask for enabling RX/TX events. Indicate which events must be forwarded to the upper layer by using @ref nrfx_nfct_evt_id_t. By default, no events are enabled. */"] + pub rxtx_int_mask: u32, + #[doc = "< Callback."] + pub cb: nrfx_nfct_handler_t, + #[doc = "< Interrupt priority."] + pub irq_priority: u8, +} +unsafe extern "C" { + #[doc = " @brief Function for initializing the NFCT driver.\n\n @param[in] p_config Pointer to the NFCT driver configuration structure.\n\n @retval 0 The NFCT driver was initialized successfully.\n @retval -EALREADY The driver is already initialized.\n @retval -EPERM The NFCT antenna pads are not configured as antenna pins.\n @retval -ECANCELED The NFCT field timer was not configured successfully.\n This is possible only if workarounds for nRF52 errata 79,\n nRF52 errata 190 or nRF53 errata 70 are enabled."] + pub fn nrfx_nfct_init(p_config: *const nrfx_nfct_config_t) -> cty::c_int; +} +unsafe extern "C" { + #[doc = " @brief Function for uninitializing the NFCT driver.\n\n After uninitialization, the instance is in disabled state."] + pub fn nrfx_nfct_uninit(); +} +unsafe extern "C" { + #[doc = " @brief Function for checking if the NFCT driver is initialized.\n\n @retval true Driver is already initialized.\n @retval false Driver is not initialized."] + pub fn nrfx_nfct_init_check() -> bool; +} +unsafe extern "C" { + #[doc = " @brief Function for starting the NFC subsystem.\n\n After this function completes, NFC readers are able to detect the tag."] + pub fn nrfx_nfct_enable(); +} +unsafe extern "C" { + #[doc = " @brief Function for disabling the NFCT driver.\n\n After this function returns, NFC readers are no longer able to connect\n to the tag."] + pub fn nrfx_nfct_disable(); +} +unsafe extern "C" { + #[doc = " @brief Function for checking whether the external NFC field is present in the range of the tag.\n\n @retval true The NFC field is present.\n @retval false No NFC field is present."] + pub fn nrfx_nfct_field_check() -> bool; +} +unsafe extern "C" { + #[doc = " @brief Function for preparing the NFCT driver for receiving an NFC frame.\n\n @param[in] p_rx_data Pointer to the RX buffer.\n\n @retval 0 The operation was successful.\n @retval -EACCES Data buffer does not point to memory region reachable by EasyDMA."] + pub fn nrfx_nfct_rx(p_rx_data: *const nrfx_nfct_data_desc_t) -> cty::c_int; +} +unsafe extern "C" { + #[doc = " @brief Function for transmitting an NFC frame.\n\n @param[in] p_tx_data Pointer to the TX buffer.\n @param[in] delay_mode Delay mode of the NFCT frame timer.\n\n @retval 0 The operation was successful.\n @retval -E2BIG The TX buffer size is invalid.\n @retval -EBUSY Driver is already transferring.\n @retval -EACCES Data buffer does not point to memory region reachable by EasyDMA."] + pub fn nrfx_nfct_tx( + p_tx_data: *const nrfx_nfct_data_desc_t, + delay_mode: nrf_nfct_frame_delay_mode_t, + ) -> cty::c_int; +} +unsafe extern "C" { + #[doc = " @brief Function for transmitting an NFC frame with a specified number of bits.\n\n @param[in] p_tx_data Pointer to the TX buffer. Unlike in @ref nrfx_nfct_tx, @p data_size is\n used as the number of bits to transmit, rather than bytes.\n @param[in] delay_mode Delay mode of the NFCT frame timer.\n\n @retval 0 The operation was successful.\n @retval -E2BIG The TX buffer size is invalid.\n @retval -EBUSY Driver is already transferring.\n @retval -EACCES Data buffer does not point to memory region reachable by EasyDMA."] + pub fn nrfx_nfct_bits_tx( + p_tx_data: *const nrfx_nfct_data_desc_t, + delay_mode: nrf_nfct_frame_delay_mode_t, + ) -> cty::c_int; +} +unsafe extern "C" { + #[doc = " @brief Function for moving the NFCT to a new state.\n\n @note The HFCLK must be running before activating the NFCT with\n @ref NRFX_NFCT_STATE_ACTIVATED.\n\n @param[in] state The required state."] + pub fn nrfx_nfct_state_force(state: nrfx_nfct_state_t); +} +unsafe extern "C" { + #[doc = " @brief Function for moving the NFCT to a new initial substate within @ref NRFX_NFCT_STATE_ACTIVATED.\n\n @param[in] sub_state The required substate."] + pub fn nrfx_nfct_init_substate_force(sub_state: nrfx_nfct_active_state_t); +} +unsafe extern "C" { + #[doc = " @brief Function for setting the NFC communication parameter.\n\n @note Parameter validation for length and acceptable values.\n\n @param[in] p_param Pointer to parameter descriptor.\n\n @retval 0 The operation was successful.\n @retval -EINVAL The parameter data is invalid."] + pub fn nrfx_nfct_parameter_set(p_param: *const nrfx_nfct_param_t) -> cty::c_int; +} +unsafe extern "C" { + #[doc = " @brief Function for getting default bytes for NFCID1.\n\n @note This function cannot be used from the non-secure code because it requires access\n to FICR registers.\n\n @param[in,out] p_nfcid1_buff In: empty buffer for data;\n Out: buffer with the NFCID1 default data. These values\n can be used to fill the Type 2 Tag Internal Bytes.\n @param[in] nfcid1_buff_len Length of the NFCID1 buffer.\n\n @retval 0 The operation was successful.\n @retval -E2BIG Length of the NFCID buffer is different than @ref NRFX_NFCT_NFCID1_SINGLE_SIZE,\n @ref NRFX_NFCT_NFCID1_DOUBLE_SIZE, or @ref NRFX_NFCT_NFCID1_TRIPLE_SIZE."] + pub fn nrfx_nfct_nfcid1_default_bytes_get( + p_nfcid1_buff: *mut u8, + nfcid1_buff_len: u32, + ) -> cty::c_int; +} +unsafe extern "C" { + #[doc = " @brief Function for enabling the automatic collision resolution.\n\n @details As defined by the NFC Forum Digital Protocol Technical Specification (and ISO 14443-3),\n the automatic collision resolution is implemented in the NFCT hardware.\n This function allows enabling and disabling this feature."] + pub fn nrfx_nfct_autocolres_enable(); +} +unsafe extern "C" { + #[doc = " @brief Function for disabling the automatic collision resolution.\n\n @details See also details in @ref nrfx_nfct_autocolres_enable."] + pub fn nrfx_nfct_autocolres_disable(); +} +unsafe extern "C" { + #[doc = " @brief Function for handling workarounds for nRF52 Anomaly 79, nRF52 Anomaly 190,\n nRF53 Anomaly 70 and nRF54L Anomaly 60.\n\n @note The function should be called from an interrupt handler for the TIMER\n instance specified by @ref NRFX_NFCT_CONFIG_TIMER_INSTANCE_ID."] + pub fn nrfx_nfct_workaround_timer_handler(); +} +unsafe extern "C" { + #[doc = " @}"] + pub fn nrfx_nfct_irq_handler(); +} diff --git a/components/solo-apps/Cargo.toml b/components/solo-apps/Cargo.toml new file mode 100644 index 0000000..bc1364b --- /dev/null +++ b/components/solo-apps/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "solo-apps" +version = "0.1.0" +authors.workspace = true +edition = "2024" + +# Shared CTAP/NFC/apps building blocks for the lpc55 + nrf52840dk runners: +# trussed dispatch, the canonical FIDO config, the client/backend plumbing, and +# the generic NDEF-suppression wrappers. Board-specific assembly (storage, HAL, +# timebase, UserInterface, Apps::new) stays in each runner. + +[dependencies] +apdu-dispatch .workspace = true +ctaphid-dispatch .workspace = true +fido-authenticator .workspace = true +littlefs2 .workspace = true +trussed .workspace = true +trussed-auth .workspace = true +trussed-auth-backend .workspace = true +trussed-chunked .workspace = true +trussed-core .workspace = true +trussed-fs-info .workspace = true +trussed-hkdf .workspace = true +trussed-hpke .workspace = true +trussed-manage .workspace = true +trussed-staging .workspace = true +trussed-wrap-key-to-file .workspace = true diff --git a/components/solo-apps/src/client.rs b/components/solo-apps/src/client.rs new file mode 100644 index 0000000..03d2997 --- /dev/null +++ b/components/solo-apps/src/client.rs @@ -0,0 +1,125 @@ +//! Shared trussed client plumbing — backend slates, the multiplexed service +//! wrapper, per-app client tags, and client registration. +//! +//! Generic over the runner's `Board` (a trussed `Platform`) and `Syscall` so the +//! board-specific pieces stay in each runner. + +use crate::dispatch::{BackendIds, Dispatch, RunnerContext}; +use trussed::backend::BackendId; +use trussed::client::{ClientTag, CurrentTagCell, MultiplexedClient, SharedRequesterCell, Syscall}; +use trussed::interrupt::InterruptFlag; +use trussed::pipe::{MultiplexedEndpoint, TrussedChannel}; +use trussed::platform::Platform; +use trussed::types::{Context, CoreContext}; + +/// Apps that need only StagingBackend (FsInfo, Hkdf, Manage) + Core. +/// `BackendId::Core` must be present or standard crypto/filesystem calls return +/// RequestNotAvailable and `syscall!()` panics. +pub static STAGING_BACKENDS: [BackendId; 2] = [ + BackendId::Custom(BackendIds::StagingBackend), + BackendId::Core, +]; + +/// secrets-app needs the Auth extension (PIN management). Auth must come first. +pub static AUTH_BACKENDS: [BackendId; 2] = + [BackendId::Custom(BackendIds::Auth), BackendId::Core]; + +/// piv-authenticator and opcard need Auth + Staging + Core. +pub static PIV_BACKENDS: [BackendId; 3] = [ + BackendId::Custom(BackendIds::Auth), + BackendId::Custom(BackendIds::StagingBackend), + BackendId::Core, +]; + +/// Multiplexed service endpoint: one shared responder, per-client contexts +/// keyed by `ClientTag`. +pub type TrussedEndpoint = MultiplexedEndpoint<'static, BackendIds, RunnerContext>; + +/// Single channel shared across every app. The requester half is stashed in +/// `SHARED_REQUESTER` so all `MultiplexedClient`s submit through it; the +/// responder half is owned by the `MultiplexedEndpoint` inside `Trussed` and +/// pumped by `Service::process_multiplexed`. +static SHARED_TRUSSED_CHANNEL: TrussedChannel = TrussedChannel::new(); +static SHARED_REQUESTER: SharedRequesterCell = SharedRequesterCell::new(); +/// Set by whichever client most recently submitted a request; read by the +/// service to find the matching context. +static CURRENT_TAG: CurrentTagCell = CurrentTagCell::new(); + +/// Per-app `ClientTag` (1..=N; 0 reserved as "no client"). Each value must be +/// distinct so `process_multiplexed` routes each request to the right context. +/// The nrf52840dk runner uses the first six; provisioner/oath-export are lpc55-only. +#[allow(dead_code)] +pub mod client_tag { + use super::ClientTag; + pub const ADMIN: ClientTag = 1; + pub const FIDO: ClientTag = 2; + pub const NDEF: ClientTag = 3; + pub const SECRETS: ClientTag = 4; + pub const PIV: ClientTag = 5; + pub const OPCARD: ClientTag = 6; + pub const PROVISIONER: ClientTag = 7; + pub const OATH_EXPORT: ClientTag = 8; +} + +/// Wrapper around the trussed `Service` that owns the multiplexed endpoint. +/// `process()` and `update_ui()` are called from the runner's RTIC OS-event +/// handler and the periodic UI task respectively. +pub struct Trussed { + service: trussed::Service, + endpoint: TrussedEndpoint, +} + +impl Trussed { + pub fn new(service: trussed::Service) -> Self { + let (req, resp) = SHARED_TRUSSED_CHANNEL + .split() + .expect("shared trussed channel already split"); + SHARED_REQUESTER.init(req); + Self { + service, + endpoint: MultiplexedEndpoint::new(resp), + } + } + + pub fn register_client( + &mut self, + tag: ClientTag, + context: Context, + backends: &'static [BackendId], + ) { + self.endpoint + .register((tag, context, backends)) + .map_err(|_| ()) + .expect("MultiplexedEndpoint full"); + } + + pub fn process(&mut self) { + self.service + .process_multiplexed(&mut self.endpoint, &CURRENT_TAG); + } + + pub fn update_ui(&mut self) { + self.service.update_ui(); + } +} + +/// Register a multiplexed client with the shared trussed service and return its +/// `MultiplexedClient`. The runner contributes the per-app `tag`, `client_id` +/// directory, optional `interrupt`, and the backends list routing extension calls. +pub fn make_client( + tag: ClientTag, + client_id: &'static littlefs2::path::Path, + trussed: &mut Trussed, + interrupt: Option<&'static InterruptFlag>, + backends: &'static [BackendId], +) -> MultiplexedClient { + let context = CoreContext::with_interrupt(littlefs2::path::PathBuf::from(client_id), interrupt); + trussed.register_client(tag, context.into(), backends); + MultiplexedClient::new( + &SHARED_REQUESTER, + &CURRENT_TAG, + tag, + S::default(), + interrupt, + ) +} diff --git a/components/solo-apps/src/config.rs b/components/solo-apps/src/config.rs new file mode 100644 index 0000000..71bda73 --- /dev/null +++ b/components/solo-apps/src/config.rs @@ -0,0 +1,41 @@ +//! Shared FIDO authenticator config. +//! +//! `nfc_transport` MUST stay `true`: NFC-on-USB works, so the authenticator must +//! advertise NFC in `getInfo`'s `transports`, or platforms record credentials as +//! non-NFC and won't offer NFC for `getAssertion` (symptom: makeCredential works +//! over NFC but getAssertion doesn't). The const guard below keeps it that way. + +/// FIDO authenticator config. `firmware_version` is the packed CTAP 2.1 §6.4 u32 +/// `(major << 22) | (minor << 6) | patch`; `max_resident` is the resident-key cap +/// (lpc55 100, nrf52840dk 50). +pub const fn fido_config(firmware_version: u32, max_resident: u32) -> fido_authenticator::Config { + fido_authenticator::Config { + max_msg_size: ctaphid_dispatch::DEFAULT_MESSAGE_SIZE, + skip_up_timeout: None, + max_resident_credential_count: Some(max_resident), + // CTAP 2.1 §6.10: minimum array size is 1024. + large_blobs: Some(fido_authenticator::LargeBlobsConfig { + location: trussed::types::Location::External, + max_size: 1024, + }), + nfc_transport: true, + ccid_transport: false, + // Struct literal, not `.into()` (not const-callable here). + firmware_version: Some(fido_authenticator::FirmwareVersion { + default: firmware_version as usize, + credential_id_v1: None, + credential_id_v2: None, + }), + // V2 credential-id format: AES-256-GCM. Applied on a clean state / after + // factory reset; existing V1 credentials persist. + credential_id_version: Some(fido_authenticator::credential::CredentialIdVersion::V2), + long_touch_for_reset: true, + fido2_up_timeout: None, + } +} + +// Regression guard — the build fails if NFC advertisement is ever dropped. +const _: () = assert!( + fido_config(0, 1).nfc_transport, + "FIDO must advertise NFC in getInfo, else getAssertion won't use NFC on phones", +); diff --git a/components/solo-apps/src/dispatch.rs b/components/solo-apps/src/dispatch.rs new file mode 100644 index 0000000..6842282 --- /dev/null +++ b/components/solo-apps/src/dispatch.rs @@ -0,0 +1,199 @@ +//! Trussed extension dispatch. +//! +//! Wires the StagingBackend (Hkdf, FsInfo, Manage, Chunked, Hpke, +//! WrapKeyToFile) and AuthBackend extensions used by fido-authenticator, +//! admin-app, secrets-app, piv-authenticator, and opcard. + +use trussed::serde_extensions::{ExtensionDispatch, ExtensionId, ExtensionImpl}; +use trussed_auth::AuthExtension; +use trussed_auth_backend::{AuthBackend, AuthContext, FilesystemLayout}; +use trussed_chunked::ChunkedExtension; +use trussed_fs_info::FsInfoExtension; +use trussed_hkdf::HkdfExtension; +use trussed_hpke::HpkeExtension; +use trussed_manage::ManageExtension; +use trussed_staging::{StagingBackend, StagingContext}; +use trussed_wrap_key_to_file::WrapKeyToFileExtension; + +pub struct Dispatch { + staging_backend: StagingBackend, + auth_backend: AuthBackend, +} + +impl Default for Dispatch { + fn default() -> Self { + Self { + staging_backend: StagingBackend::new(), + auth_backend: AuthBackend::new( + trussed::types::Location::Internal, + FilesystemLayout::V0, + ), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BackendIds { + StagingBackend, + Auth, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExtensionIds { + Auth = 0, + Hkdf = 1, + Manage = 2, + WrapKeyToFile = 3, + FsInfo = 4, + Hpke = 5, + Chunked = 6, +} + +impl From for u8 { + fn from(id: ExtensionIds) -> u8 { + id as u8 + } +} + +impl TryFrom for ExtensionIds { + type Error = trussed::Error; + fn try_from(id: u8) -> Result { + match id { + 0 => Ok(Self::Auth), + 1 => Ok(Self::Hkdf), + 2 => Ok(Self::Manage), + 3 => Ok(Self::WrapKeyToFile), + 4 => Ok(Self::FsInfo), + 5 => Ok(Self::Hpke), + 6 => Ok(Self::Chunked), + _ => Err(trussed::Error::FunctionNotSupported), + } + } +} + +impl ExtensionId for Dispatch { + type Id = ExtensionIds; + const ID: ExtensionIds = ExtensionIds::Auth; +} +impl ExtensionId for Dispatch { + type Id = ExtensionIds; + const ID: ExtensionIds = ExtensionIds::Chunked; +} +impl ExtensionId for Dispatch { + type Id = ExtensionIds; + const ID: ExtensionIds = ExtensionIds::FsInfo; +} +impl ExtensionId for Dispatch { + type Id = ExtensionIds; + const ID: ExtensionIds = ExtensionIds::Hkdf; +} +impl ExtensionId for Dispatch { + type Id = ExtensionIds; + const ID: ExtensionIds = ExtensionIds::Hpke; +} +impl ExtensionId for Dispatch { + type Id = ExtensionIds; + const ID: ExtensionIds = ExtensionIds::Manage; +} +impl ExtensionId for Dispatch { + type Id = ExtensionIds; + const ID: ExtensionIds = ExtensionIds::WrapKeyToFile; +} + +#[derive(Default)] +pub struct RunnerContext { + pub auth: AuthContext, + pub staging: StagingContext, +} + +impl ExtensionDispatch for Dispatch { + type BackendId = BackendIds; + type Context = RunnerContext; + type ExtensionId = ExtensionIds; + + fn core_request( + &mut self, + backend: &Self::BackendId, + ctx: &mut trussed::types::Context, + request: &trussed::api::Request, + resources: &mut trussed::service::ServiceResources

, + ) -> Result { + use trussed::backend::Backend; + match backend { + BackendIds::StagingBackend => self.staging_backend.request( + &mut ctx.core, + &mut ctx.backends.staging, + request, + resources, + ), + BackendIds::Auth => { + self.auth_backend + .request(&mut ctx.core, &mut ctx.backends.auth, request, resources) + } + } + } + + fn extension_request( + &mut self, + _backend: &Self::BackendId, + extension: &Self::ExtensionId, + ctx: &mut trussed::types::Context, + request: &trussed::api::request::SerdeExtension, + resources: &mut trussed::service::ServiceResources

, + ) -> Result { + match extension { + ExtensionIds::Auth => self.auth_backend.extension_request_serialized( + &mut ctx.core, + &mut ctx.backends.auth, + request, + resources, + ), + ExtensionIds::FsInfo => ExtensionImpl::::extension_request_serialized( + &mut self.staging_backend, + &mut ctx.core, + &mut ctx.backends.staging, + request, + resources, + ), + ExtensionIds::Hkdf => ExtensionImpl::::extension_request_serialized( + &mut self.staging_backend, + &mut ctx.core, + &mut ctx.backends.staging, + request, + resources, + ), + ExtensionIds::Hpke => ExtensionImpl::::extension_request_serialized( + &mut self.staging_backend, + &mut ctx.core, + &mut ctx.backends.staging, + request, + resources, + ), + ExtensionIds::Manage => ExtensionImpl::::extension_request_serialized( + &mut self.staging_backend, + &mut ctx.core, + &mut ctx.backends.staging, + request, + resources, + ), + ExtensionIds::WrapKeyToFile => { + ExtensionImpl::::extension_request_serialized( + &mut self.staging_backend, + &mut ctx.core, + &mut ctx.backends.staging, + request, + resources, + ) + } + ExtensionIds::Chunked => { + ExtensionImpl::::extension_request_serialized( + &mut self.staging_backend, + &mut ctx.core, + &mut ctx.backends.staging, + request, + resources, + ) + } + } + } +} diff --git a/components/solo-apps/src/lib.rs b/components/solo-apps/src/lib.rs new file mode 100644 index 0000000..1ac1fcd --- /dev/null +++ b/components/solo-apps/src/lib.rs @@ -0,0 +1,13 @@ +#![no_std] + +//! Shared CTAP/NFC/apps building blocks for the lpc55 and nrf52840dk runners. +//! +//! Provides the trussed dispatch, the canonical FIDO config, the client/backend +//! plumbing, and the generic NDEF-suppression wrappers. Board-specific assembly +//! (storage, HAL, NDEF timebase, `UserInterface`, `Apps::new`) stays in each +//! runner. + +pub mod client; +pub mod config; +pub mod dispatch; +pub mod ndef; diff --git a/components/solo-apps/src/ndef.rs b/components/solo-apps/src/ndef.rs new file mode 100644 index 0000000..f615908 --- /dev/null +++ b/components/solo-apps/src/ndef.rs @@ -0,0 +1,113 @@ +//! NDEF-suppression wrappers shared by both runners. +//! +//! The NDEF app refuses `SELECT` (so phones don't pop the tag during/after a +//! FIDO ceremony) while `now() - last_fido() < SUPPRESS_WINDOW`. The timebase +//! and the FIDO-transport hook are board-specific and supplied via `NfcClock`; +//! the wrappers themselves carry no per-instance state. + +use core::marker::PhantomData; +use core::sync::atomic::{AtomicU32, Ordering::Relaxed}; + +/// Board-specific NDEF timebase + FIDO-transport hook. +pub trait NfcClock { + /// Suppression window, same unit as `now()` (lpc55 7 s, nrf52840dk 3000 ms). + const SUPPRESS_WINDOW: u32; + /// Free-running counter, valid in passive (lpc55 RTC COUNT, nrf52840dk ms task). + fn now() -> u32; + /// Board-owned static stamped on every FIDO select/call. Initialised + /// `now() - SUPPRESS_WINDOW` "in the past" so the tag is readable at boot. + fn last_fido() -> &'static AtomicU32; + /// Record whether the in-flight FIDO request arrived contactless, so + /// `check_user_presence` takes the tap as presence. Default no-op. + fn set_fido_over_nfc(_contactless: bool) {} +} + +/// Wraps the FIDO app: stamps the suppression window on every FIDO select/call. +pub struct FidoNdefStamp<'a, C: NfcClock, A>(&'a mut A, PhantomData); + +impl<'a, C: NfcClock, A> FidoNdefStamp<'a, C, A> { + pub fn new(app: &'a mut A) -> Self { + Self(app, PhantomData) + } +} + +impl apdu_dispatch::iso7816::App + for FidoNdefStamp<'_, C, A> +{ + fn aid(&self) -> apdu_dispatch::iso7816::Aid { + self.0.aid() + } +} + +impl apdu_dispatch::app::App for FidoNdefStamp<'_, C, A> { + fn select( + &mut self, + interface: apdu_dispatch::app::Interface, + command: apdu_dispatch::app::CommandView<'_>, + reply: &mut apdu_dispatch::app::VecView, + ) -> apdu_dispatch::app::Result { + C::last_fido().store(C::now(), Relaxed); + self.0.select(interface, command, reply) + } + fn deselect(&mut self) { + self.0.deselect() + } + fn call( + &mut self, + interface: apdu_dispatch::app::Interface, + command: apdu_dispatch::app::CommandView<'_>, + reply: &mut apdu_dispatch::app::VecView, + ) -> apdu_dispatch::app::Result { + C::last_fido().store(C::now(), Relaxed); + // Record the transport so check_user_presence takes the tap as presence + // for an NFC (contactless) request and requires a button otherwise. + C::set_fido_over_nfc(matches!( + interface, + apdu_dispatch::app::Interface::Contactless + )); + self.0.call(interface, command, reply) + } +} + +/// Wraps the NDEF app: refuses `SELECT` (so phones see no tag) while suppressed. +pub struct NdefFidoGate<'a, C: NfcClock, A>(&'a mut A, PhantomData); + +impl<'a, C: NfcClock, A> NdefFidoGate<'a, C, A> { + pub fn new(app: &'a mut A) -> Self { + Self(app, PhantomData) + } +} + +impl apdu_dispatch::iso7816::App + for NdefFidoGate<'_, C, A> +{ + fn aid(&self) -> apdu_dispatch::iso7816::Aid { + self.0.aid() + } +} + +impl apdu_dispatch::app::App for NdefFidoGate<'_, C, A> { + fn select( + &mut self, + interface: apdu_dispatch::app::Interface, + command: apdu_dispatch::app::CommandView<'_>, + reply: &mut apdu_dispatch::app::VecView, + ) -> apdu_dispatch::app::Result { + let since = C::now().wrapping_sub(C::last_fido().load(Relaxed)); + if since < C::SUPPRESS_WINDOW { + return Err(apdu_dispatch::iso7816::Status::NotFound); + } + self.0.select(interface, command, reply) + } + fn deselect(&mut self) { + self.0.deselect() + } + fn call( + &mut self, + interface: apdu_dispatch::app::Interface, + command: apdu_dispatch::app::CommandView<'_>, + reply: &mut apdu_dispatch::app::VecView, + ) -> apdu_dispatch::app::Result { + self.0.call(interface, command, reply) + } +} diff --git a/runners/lpc55/Cargo.toml b/runners/lpc55/Cargo.toml index e283cf8..4ccbd53 100644 --- a/runners/lpc55/Cargo.toml +++ b/runners/lpc55/Cargo.toml @@ -54,6 +54,7 @@ trussed-core .workspace = true board.workspace = true # components +solo-apps .workspace = true ndef-app = { workspace = true, optional = true } # NB: when using this app, need to raise trussed/clients-5 provisioner-app = { workspace = true, optional = true } diff --git a/runners/lpc55/README.md b/runners/lpc55/README.md index 128b24d..e0939c5 100644 --- a/runners/lpc55/README.md +++ b/runners/lpc55/README.md @@ -1,29 +1,24 @@ -# LPC55 runner +# solo2 on NXP LPC55S69 -The entire firmware that runs all the things. +### the open source FIDO2 security key — built with Trussed® -## Solo 2 Hacker builds +Solo 2C Hacker +   +LPCXpresso55S69-EVK -For a standalone Solo 2 Hacker, the safest custom-firmware path is the `develop` -variant because it disables encrypted storage (`no-encrypted-storage`). +This is the firmware for the **LPC55S69** — the NXP Cortex-M33 silicon inside +the shipping **[Solo 2](https://solokeys.com)**. It runs on two boards: -- `make build-hacker` - Builds `board-solo2,develop` and writes `app-solo2.bin`. -- `make build-hacker-recovery` - Builds `board-solo2,develop,format-filesystem` and forces a filesystem format on first boot. -- `make build-release` - Builds the PRINCE/PUF-backed production-style image. Use this only if the device is - provisioned for encrypted storage. +- **Solo 2 Hacker** — the real SoloKeys device, unlocked for developers + (PRINCE-encrypted storage, no debug port). Get one from + [solokeys.com](https://solokeys.com). +- **LPCXpresso55S69-EVK** — NXP's evaluation board with the same chip and an + onboard J-Link, for recoverable development. Get one from + [nxp.com](https://www.nxp.com/products/processors-and-microcontrollers/arm-based-processors-and-mcus/lpc-cortex-m-mcus/lpc5500-cortex-m33/lpcxpresso55s69-development-board:LPC55S69-EVK). +## Build -### Logging - -The easy + fast way to log is to use the `log-rtt` feature. -Listening on port `19021` (e.g. via `netcat localhost 19021`) outputs the RTT message output -from `JLinkGDBServer -strict -device LPC55S69 -if SWD -vd`. - -The slower alternative (although not so bad due to `delog` bundling) is to use the `log-semihosting` feature. -Both at once does not work, neither does `log-serial`. - -Additionally, logging features need to be turned on. -An example invocation: `cargo run --release --features board-lpcxpresso55,develop,log-rtt,fido-authenticator/log-all` +``` +make build-dev # EVK / development build (plain storage) +make build-release # Solo 2 Hacker, PRINCE-encrypted (provisioned devices) +``` diff --git a/runners/lpc55/src/types.rs b/runners/lpc55/src/types.rs index 0b7674b..ce1cc50 100644 --- a/runners/lpc55/src/types.rs +++ b/runners/lpc55/src/types.rs @@ -4,23 +4,10 @@ use crate::hal; use hal::drivers::timer; use hal::peripherals::ctimer; use littlefs2::{const_ram_storage, consts}; -use trussed::backend::BackendId; -use trussed::client::{ClientTag, CurrentTagCell, MultiplexedClient, SharedRequesterCell}; +use trussed::client::MultiplexedClient; use trussed::interrupt::InterruptFlag; -use trussed::pipe::{MultiplexedEndpoint, TrussedChannel}; use trussed::platform; -use trussed::serde_extensions::{ExtensionDispatch, ExtensionId, ExtensionImpl}; use trussed::store::DynFilesystem; -use trussed::types::CoreContext; -use trussed_auth::AuthExtension; -use trussed_auth_backend::{AuthBackend, AuthContext, FilesystemLayout}; -use trussed_chunked::ChunkedExtension; -use trussed_fs_info::FsInfoExtension; -use trussed_hkdf::HkdfExtension; -use trussed_hpke::HpkeExtension; -use trussed_manage::ManageExtension; -use trussed_staging::{StagingBackend, StagingContext}; -use trussed_wrap_key_to_file::WrapKeyToFileExtension; // Compile time assertion that build_constants::CONFIG_FILESYSTEM_BOUNDARY is 512 byte aligned. const _FILESYSTEM_ALIGNED_CHECK: usize = ((core::mem::size_of::< @@ -258,200 +245,10 @@ platform!(Board, UI: board::trussed::UserInterface, ); -/// Extension dispatch type providing FsInfo, Hkdf, Manage (via trussed-staging) and -/// Auth (via trussed-auth-backend) extensions. -/// Required because fido-authenticator 0.2 unconditionally needs FsInfoClient + HkdfClient, -/// admin-app requires ManageClient, and secrets-app requires AuthClient. -pub struct Dispatch { - staging_backend: StagingBackend, - auth_backend: AuthBackend, -} - -impl Default for Dispatch { - fn default() -> Self { - Self { - staging_backend: StagingBackend::new(), - // V0 layout: new device, no existing auth data to migrate - auth_backend: AuthBackend::new( - trussed::types::Location::Internal, - FilesystemLayout::V0, - ), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BackendIds { - StagingBackend, - Auth, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ExtensionIds { - Auth = 0, - Hkdf = 1, - Manage = 2, - WrapKeyToFile = 3, - FsInfo = 4, - Hpke = 5, - Chunked = 6, -} - -impl From for u8 { - fn from(id: ExtensionIds) -> u8 { - id as u8 - } -} - -impl TryFrom for ExtensionIds { - type Error = trussed::Error; - fn try_from(id: u8) -> Result { - match id { - 0 => Ok(Self::Auth), - 1 => Ok(Self::Hkdf), - 2 => Ok(Self::Manage), - 3 => Ok(Self::WrapKeyToFile), - 4 => Ok(Self::FsInfo), - 5 => Ok(Self::Hpke), - 6 => Ok(Self::Chunked), - _ => Err(trussed::Error::FunctionNotSupported), - } - } -} - -impl ExtensionId for Dispatch { - type Id = ExtensionIds; - const ID: ExtensionIds = ExtensionIds::Auth; -} - -impl ExtensionId for Dispatch { - type Id = ExtensionIds; - const ID: ExtensionIds = ExtensionIds::Chunked; -} - -impl ExtensionId for Dispatch { - type Id = ExtensionIds; - const ID: ExtensionIds = ExtensionIds::FsInfo; -} - -impl ExtensionId for Dispatch { - type Id = ExtensionIds; - const ID: ExtensionIds = ExtensionIds::Hkdf; -} - -impl ExtensionId for Dispatch { - type Id = ExtensionIds; - const ID: ExtensionIds = ExtensionIds::Hpke; -} - -impl ExtensionId for Dispatch { - type Id = ExtensionIds; - const ID: ExtensionIds = ExtensionIds::Manage; -} - -impl ExtensionId for Dispatch { - type Id = ExtensionIds; - const ID: ExtensionIds = ExtensionIds::WrapKeyToFile; -} - -/// Combined context for all backends in the dispatch. -#[derive(Default)] -pub struct RunnerContext { - pub auth: AuthContext, - pub staging: StagingContext, -} - -impl ExtensionDispatch for Dispatch { - type BackendId = BackendIds; - type Context = RunnerContext; - type ExtensionId = ExtensionIds; - - fn core_request( - &mut self, - backend: &Self::BackendId, - ctx: &mut trussed::types::Context, - request: &trussed::api::Request, - resources: &mut trussed::service::ServiceResources

, - ) -> Result { - use trussed::backend::Backend; - match backend { - BackendIds::StagingBackend => self.staging_backend.request( - &mut ctx.core, - &mut ctx.backends.staging, - request, - resources, - ), - BackendIds::Auth => { - self.auth_backend - .request(&mut ctx.core, &mut ctx.backends.auth, request, resources) - } - } - } - - fn extension_request( - &mut self, - _backend: &Self::BackendId, - extension: &Self::ExtensionId, - ctx: &mut trussed::types::Context, - request: &trussed::api::request::SerdeExtension, - resources: &mut trussed::service::ServiceResources

, - ) -> Result { - match extension { - ExtensionIds::Auth => self.auth_backend.extension_request_serialized( - &mut ctx.core, - &mut ctx.backends.auth, - request, - resources, - ), - ExtensionIds::FsInfo => ExtensionImpl::::extension_request_serialized( - &mut self.staging_backend, - &mut ctx.core, - &mut ctx.backends.staging, - request, - resources, - ), - ExtensionIds::Hkdf => ExtensionImpl::::extension_request_serialized( - &mut self.staging_backend, - &mut ctx.core, - &mut ctx.backends.staging, - request, - resources, - ), - ExtensionIds::Manage => ExtensionImpl::::extension_request_serialized( - &mut self.staging_backend, - &mut ctx.core, - &mut ctx.backends.staging, - request, - resources, - ), - ExtensionIds::Chunked => { - ExtensionImpl::::extension_request_serialized( - &mut self.staging_backend, - &mut ctx.core, - &mut ctx.backends.staging, - request, - resources, - ) - } - ExtensionIds::Hpke => ExtensionImpl::::extension_request_serialized( - &mut self.staging_backend, - &mut ctx.core, - &mut ctx.backends.staging, - request, - resources, - ), - ExtensionIds::WrapKeyToFile => { - ExtensionImpl::::extension_request_serialized( - &mut self.staging_backend, - &mut ctx.core, - &mut ctx.backends.staging, - request, - resources, - ) - } - } - } -} +// Trussed extension dispatch — shared with the nrf52840dk runner via `solo-apps`. +use solo_apps::client::{client_tag, make_client}; +pub use solo_apps::dispatch::{BackendIds, Dispatch, RunnerContext}; +use solo_apps::ndef::{FidoNdefStamp, NdefFidoGate}; #[derive(Default)] pub struct Syscall {} @@ -463,87 +260,13 @@ impl trussed::client::Syscall for Syscall { } } -/// Multiplexed service endpoint: one shared responder, per-client contexts -/// keyed by `ClientTag`. -pub type TrussedEndpoint = MultiplexedEndpoint<'static, BackendIds, RunnerContext>; -/// Client type for apps — all apps share the SHARED_TRUSSED_CHANNEL and are -/// distinguished by a per-app `ClientTag` so the service can route requests -/// to the matching context. +/// Client type for apps — all apps share the channel inside the multiplexed +/// service and are distinguished by a per-app `ClientTag`. pub type TrussedClient = MultiplexedClient; -/// Backends for most apps: StagingBackend (FsInfo, Hkdf, Manage) + Core. -/// BackendId::Core must be present or all standard crypto/filesystem calls return -/// RequestNotAvailable, causing syscall!() to panic and the device to freeze. -static STAGING_BACKENDS: [BackendId; 2] = [ - BackendId::Custom(BackendIds::StagingBackend), - BackendId::Core, -]; - -/// Backends for apps requiring Auth extension (secrets-app). -/// Auth must come first so AuthClient calls reach AuthBackend. -#[cfg(feature = "oath")] -static AUTH_BACKENDS: [BackendId; 2] = - [BackendId::Custom(BackendIds::Auth), BackendId::Core]; - -/// Backends for piv-authenticator: needs Auth (PIN management), Staging (Chunked/Hpke/WrapKeyToFile), -/// and Core (standard crypto/filesystem). -#[cfg(feature = "piv-authenticator")] -static PIV_BACKENDS: [BackendId; 3] = [ - BackendId::Custom(BackendIds::Auth), - BackendId::Custom(BackendIds::StagingBackend), - BackendId::Core, -]; - -/// Backends for opcard: same requirements as PIV (Auth for PIN, Staging for Chunked/WrapKeyToFile, -/// Core for standard crypto/filesystem). -#[cfg(feature = "opcard")] -static OPCARD_BACKENDS: [BackendId; 3] = [ - BackendId::Custom(BackendIds::Auth), - BackendId::Custom(BackendIds::StagingBackend), - BackendId::Core, -]; - -/// Wrapper around the trussed Service that owns the multiplexed endpoint. -/// `process()` and `update_ui()` are called from the RTIC OS_EVENT handler and -/// the periodic UI task respectively. -pub struct Trussed { - service: trussed::Service, - endpoint: TrussedEndpoint, -} - -impl Trussed { - pub fn new(service: trussed::Service) -> Self { - let (req, resp) = SHARED_TRUSSED_CHANNEL - .split() - .expect("shared trussed channel already split"); - SHARED_REQUESTER.init(req); - Self { - service, - endpoint: MultiplexedEndpoint::new(resp), - } - } - - pub fn register_client( - &mut self, - tag: ClientTag, - context: trussed::types::Context, - backends: &'static [BackendId], - ) { - self.endpoint - .register((tag, context, backends)) - .map_err(|_| ()) - .expect("MultiplexedEndpoint full"); - } - - pub fn process(&mut self) { - self.service - .process_multiplexed(&mut self.endpoint, &CURRENT_TAG); - } - - pub fn update_ui(&mut self) { - self.service.update_ui(); - } -} +// Backend slates, the multiplexed service wrapper (`Trussed`), `client_tag`, and +// `make_client` are shared with the nrf52840dk runner via `solo_apps::client`. +pub type Trussed = solo_apps::client::Trussed; pub type Iso14443 = nfc_device::Iso14443<'static, board::nfc::NfcChip>; @@ -592,42 +315,10 @@ pub type FidoApp = fido_authenticator::Authenticator; @@ -650,74 +341,20 @@ fn rtc_secs() -> u32 { unsafe { (*hal::raw::RTC::ptr()).count.read().bits() } } -/// Wraps the FIDO app: marks NDEF suppression active on every FIDO select/call. -pub struct FidoNdefStamp<'a, A>(pub &'a mut A); -impl apdu_dispatch::iso7816::App for FidoNdefStamp<'_, A> { - fn aid(&self) -> apdu_dispatch::iso7816::Aid { - self.0.aid() +/// lpc55 NDEF timebase + FIDO-transport hook for `solo_apps::ndef`. `now()` is +/// the always-on RTC COUNT (1 Hz, valid in passive); `set_fido_over_nfc` records +/// the transport so `check_user_presence` takes an NFC tap as presence. +pub struct LpcNfcClock; +impl solo_apps::ndef::NfcClock for LpcNfcClock { + const SUPPRESS_WINDOW: u32 = NDEF_SUPPRESS_SECS; + fn now() -> u32 { + rtc_secs() } -} -impl apdu_dispatch::app::App for FidoNdefStamp<'_, A> { - fn select( - &mut self, - interface: apdu_dispatch::app::Interface, - command: apdu_dispatch::app::CommandView<'_>, - reply: &mut apdu_dispatch::app::VecView, - ) -> apdu_dispatch::app::Result { - NDEF_LAST_FIDO_SEC.store(rtc_secs(), core::sync::atomic::Ordering::Relaxed); - self.0.select(interface, command, reply) + fn last_fido() -> &'static core::sync::atomic::AtomicU32 { + &NDEF_LAST_FIDO_SEC } - fn deselect(&mut self) { - self.0.deselect() - } - fn call( - &mut self, - interface: apdu_dispatch::app::Interface, - command: apdu_dispatch::app::CommandView<'_>, - reply: &mut apdu_dispatch::app::VecView, - ) -> apdu_dispatch::app::Result { - NDEF_LAST_FIDO_SEC.store(rtc_secs(), core::sync::atomic::Ordering::Relaxed); - // Record the transport so check_user_presence takes the tap as presence - // for an NFC (contactless) request and requires a button otherwise. - board::trussed::FIDO_OVER_NFC.store( - matches!(interface, apdu_dispatch::app::Interface::Contactless), - core::sync::atomic::Ordering::Relaxed, - ); - self.0.call(interface, command, reply) - } -} - -/// Wraps the NDEF app: refuses `SELECT` (so phones see no tag) while suppressed. -pub struct NdefFidoGate<'a, A>(pub &'a mut A); -impl apdu_dispatch::iso7816::App for NdefFidoGate<'_, A> { - fn aid(&self) -> apdu_dispatch::iso7816::Aid { - self.0.aid() - } -} -impl apdu_dispatch::app::App for NdefFidoGate<'_, A> { - fn select( - &mut self, - interface: apdu_dispatch::app::Interface, - command: apdu_dispatch::app::CommandView<'_>, - reply: &mut apdu_dispatch::app::VecView, - ) -> apdu_dispatch::app::Result { - use core::sync::atomic::Ordering::Relaxed; - let since = rtc_secs().wrapping_sub(NDEF_LAST_FIDO_SEC.load(Relaxed)); - if since < NDEF_SUPPRESS_SECS { - return Err(apdu_dispatch::iso7816::Status::NotFound); - } - self.0.select(interface, command, reply) - } - fn deselect(&mut self) { - self.0.deselect() - } - fn call( - &mut self, - interface: apdu_dispatch::app::Interface, - command: apdu_dispatch::app::CommandView<'_>, - reply: &mut apdu_dispatch::app::VecView, - ) -> apdu_dispatch::app::Result { - self.0.call(interface, command, reply) + fn set_fido_over_nfc(contactless: bool) { + board::trussed::FIDO_OVER_NFC.store(contactless, core::sync::atomic::Ordering::Relaxed); } } #[cfg(feature = "provisioner-app")] @@ -730,31 +367,6 @@ pub type DynamicClockController = board::clock_controller::DynamicClockControlle pub type NfcWaitExtender = timer::Timer>; pub type PerformanceTimer = timer::Timer>; -/// Single channel shared across every app. The requester half is stashed in -/// `SHARED_REQUESTER` so all `MultiplexedClient`s can submit requests through -/// it; the responder half is owned by the `MultiplexedEndpoint` inside -/// `Trussed` and pumped by `Service::process_multiplexed`. -static SHARED_TRUSSED_CHANNEL: TrussedChannel = TrussedChannel::new(); -static SHARED_REQUESTER: SharedRequesterCell = SharedRequesterCell::new(); -/// Set by whichever client most recently submitted a request; read by the -/// service to find the matching context. -static CURRENT_TAG: CurrentTagCell = CurrentTagCell::new(); - -/// Per-app ClientTag (1..=N; 0 reserved as "no client"). Each value must be -/// distinct so `process_multiplexed` can route requests to the right context. -#[allow(dead_code)] -mod client_tag { - use super::ClientTag; - pub const ADMIN: ClientTag = 1; - pub const FIDO: ClientTag = 2; - pub const SECRETS: ClientTag = 4; - pub const PIV: ClientTag = 5; - pub const OPCARD: ClientTag = 6; - pub const PROVISIONER: ClientTag = 7; - pub const OATH_EXPORT: ClientTag = 8; - pub const NDEF: ClientTag = 3; -} - #[cfg(feature = "admin-app")] static ADMIN_INTERRUPT: InterruptFlag = InterruptFlag::new(); #[cfg(feature = "ndef-app")] @@ -772,28 +384,6 @@ static SECRETS_INTERRUPT: InterruptFlag = InterruptFlag::new(); #[cfg(feature = "oath-export")] static OATH_EXPORT_INTERRUPT: InterruptFlag = InterruptFlag::new(); -/// Register a multiplexed client with the shared trussed service and return -/// the corresponding `MultiplexedClient`. The runner contributes the -/// per-app `tag`, `client_id` directory, optional `interrupt`, and the -/// backends list used to route extension calls. -fn make_client( - tag: ClientTag, - client_id: &'static littlefs2::path::Path, - trussed: &mut Trussed, - interrupt: Option<&'static InterruptFlag>, - backends: &'static [BackendId], -) -> TrussedClient { - let context = CoreContext::with_interrupt(littlefs2::path::PathBuf::from(client_id), interrupt); - trussed.register_client(tag, context.into(), backends); - MultiplexedClient::new( - &SHARED_REQUESTER, - &CURRENT_TAG, - tag, - Syscall::default(), - interrupt, - ) -} - pub struct ProvisionerNonPortable { pub store: Store, pub stolen_filesystem: &'static mut FlashStorage, @@ -832,7 +422,7 @@ impl Apps { littlefs2::path!("admin"), trussed, Some(&ADMIN_INTERRUPT), - &STAGING_BACKENDS, + &solo_apps::client::STAGING_BACKENDS, ); AdminApp::with_default_config( client, @@ -851,7 +441,7 @@ impl Apps { littlefs2::path!("fido"), trussed, Some(&FIDO_INTERRUPT), - &STAGING_BACKENDS, + &solo_apps::client::STAGING_BACKENDS, ); fido_authenticator::Authenticator::new( client, @@ -867,7 +457,7 @@ impl Apps { littlefs2::path!("piv"), trussed, Some(&PIV_INTERRUPT), - &PIV_BACKENDS, + &solo_apps::client::PIV_BACKENDS, ); PivApp::new( client, @@ -882,7 +472,7 @@ impl Apps { littlefs2::path!("opcard"), trussed, Some(&OPCARD_INTERRUPT), - &OPCARD_BACKENDS, + &solo_apps::client::PIV_BACKENDS, ); { let mut opts = opcard::Options::default(); @@ -898,7 +488,7 @@ impl Apps { littlefs2::path!("secrets"), trussed, Some(&SECRETS_INTERRUPT), - &AUTH_BACKENDS, + &solo_apps::client::AUTH_BACKENDS, ); let uuid = hal::uuid(); SecretsApp::new( @@ -920,7 +510,7 @@ impl Apps { littlefs2::path!("ndef"), trussed, Some(&NDEF_INTERRUPT), - &STAGING_BACKENDS, + &solo_apps::client::STAGING_BACKENDS, ); NdefApp::new(client) }; @@ -932,7 +522,7 @@ impl Apps { littlefs2::path!("attn"), trussed, Some(&PROVISIONER_INTERRUPT), - &STAGING_BACKENDS, + &solo_apps::client::STAGING_BACKENDS, ); let ProvisionerNonPortable { store, @@ -949,7 +539,7 @@ impl Apps { littlefs2::path!("oathmig"), trussed, Some(&OATH_EXPORT_INTERRUPT), - &STAGING_BACKENDS, + &solo_apps::client::STAGING_BACKENDS, ); OathExportApp::new(client, store) }; @@ -981,7 +571,7 @@ impl Apps { { f(&mut [ #[cfg(feature = "ndef-app")] - &mut NdefFidoGate(&mut self.ndef), + &mut NdefFidoGate::::new(&mut self.ndef), #[cfg(feature = "piv-authenticator")] &mut self.piv, #[cfg(feature = "opcard")] @@ -989,7 +579,7 @@ impl Apps { #[cfg(feature = "oath")] &mut self.secrets, #[cfg(feature = "fido-authenticator")] - &mut FidoNdefStamp(&mut self.fido), + &mut FidoNdefStamp::::new(&mut self.fido), #[cfg(feature = "admin-app")] &mut self.admin, #[cfg(feature = "provisioner-app")] diff --git a/runners/nrf52840dk/.cargo/config.toml b/runners/nrf52840dk/.cargo/config.toml new file mode 100644 index 0000000..b797385 --- /dev/null +++ b/runners/nrf52840dk/.cargo/config.toml @@ -0,0 +1,15 @@ +[build] +target = "thumbv7em-none-eabihf" + +[target.thumbv7em-none-eabihf] +# cortex-m-rt 0.7+ injects -Tlink.x itself via build.rs; nrf52840-hal's build.rs +# adds memory.x to the link search path. Don't duplicate either here. +rustflags = [ + "-C", "linker=flip-link", +] + +[env] +# defmt 1.x defaults to Error level when DEFMT_LOG is unset — info!/debug!/trace! +# get compiled to nothing and the RTT buffer stays silent. Pin it to info for +# this runner so bring-up logs (USB init, NFCT state machine, etc.) reach RTT. +DEFMT_LOG = "info" diff --git a/runners/nrf52840dk/Cargo.toml b/runners/nrf52840dk/Cargo.toml new file mode 100644 index 0000000..b6c6a15 --- /dev/null +++ b/runners/nrf52840dk/Cargo.toml @@ -0,0 +1,97 @@ +[package] +name = "runner-nrf52840dk" +version.workspace = true +authors.workspace = true +edition.workspace = true +description = "Solo2 firmware port to Nordic nRF52840-DK" + +[[bin]] +name = "runner-nrf52840dk" +path = "src/main.rs" + +[features] +default = ["board-dk", "ccid"] +# Pick exactly one of these to target a board. Mutually exclusive — see +# the compile_error! in src/board/mod.rs. +board-dk = [] # Nordic nRF52840-DK (PCA10056) dev board +board-solo = [] # SoloKeys USB custom board + +# USB-CCID (contact smartcard) alongside CTAPHID. ON by default. CCID's USB +# EasyDMA traffic contends with NFCT EasyDMA on the RAM bus, which makes NFC +# flaky on strict bench readers (e.g. ACR1252, ~3/5) — but phones retry through +# it, so FIDO2-over-NFC including registration works. For max NFC reliability +# (no contact reader; solid on strict readers too) build NFC-only by omitting +# this feature — see the `build-dk-nfc-only` make target. +ccid = ["dep:usbd-ccid"] + +# Replaces button-driven user presence with a probe-rs-writable static +# (`UP_CONTROL` in `board::mod`). Used by the integration tests in +# `runners/pc/tests` to drive `approve`/`deny` from the host over JTAG. +# Off by default; NEVER enable on firmware shipped to users. +test-up-control = [] + +# Compile out the CTAP2 reset 10s-after-boot window so the device-test +# harness can authenticatorReset between cases (long-touch / Strong consent). +# Test/dev builds only — NEVER ship. +no-reset-time-window = ["fido-authenticator/disable-reset-time-window"] + +[dependencies] +cortex-m .workspace = true +cortex-m-rt = "0.7" +defmt .workspace = true +defmt-rtt .workspace = true +panic-halt .workspace = true +rtic = { version = "2.0.0", features = ["thumbv7-backend"] } +rtic-monotonics .workspace = true +rtic-sync .workspace = true +embedded-time = "0.12" + +embedded-hal .workspace = true +embedded-storage = "0.3" + +# USB + CTAPHID + APDU dispatch +usb-device .workspace = true +usbd-ctaphid .workspace = true +usbd-ccid = { workspace = true, optional = true } +ctaphid-dispatch .workspace = true +apdu-dispatch .workspace = true +interchange .workspace = true + +# Shared CTAP/NFC/apps building blocks (dispatch, fido config, ndef wrappers). +solo-apps .workspace = true + +# NFC stack — chip layer (libnrfx_nfct.a) + Type 4 Tag library +# (libnfc_t4t.a) + Rust platform glue. +nrf-nfc .workspace = true + +# Trussed + storage + apps +littlefs2 .workspace = true +# `mldsa44` (CTAP 2.3) is opt-in per runner — see workspace Cargo.toml. +# DK has plenty of headroom; LPC55 doesn't fit it. +trussed = { workspace = true, features = ["mldsa44"] } +trussed-core = { workspace = true, features = ["mldsa44"] } +trussed-staging .workspace = true +trussed-fs-info .workspace = true +trussed-hkdf .workspace = true +trussed-manage .workspace = true +trussed-chunked .workspace = true +trussed-hpke .workspace = true +trussed-wrap-key-to-file .workspace = true +trussed-auth .workspace = true +trussed-auth-backend .workspace = true +fido-authenticator = { workspace = true, features = ["mldsa44"] } +admin-app .workspace = true +ndef-app .workspace = true +secrets-app .workspace = true +piv-authenticator .workspace = true +opcard .workspace = true +heapless .workspace = true +generic-array = "0.14" +static_cell .workspace = true + +# nRF52840 HAL/PAC. Pinned for usb-device 0.2 + usbd-ctaphid 0.4 compatibility: +# nrf-usbd 0.1 is the only usb-device 0.2-compatible nRF USB driver, and it +# pairs with nrf52840-hal 0.15.1. nrf-hal-common 0.15.1 accepts cortex-m-rt 0.7. +nrf52840-hal = "0.15.1" +nrf52840-pac = "0.11" +nrf-usbd = "0.1" diff --git a/runners/nrf52840dk/Makefile b/runners/nrf52840dk/Makefile new file mode 100644 index 0000000..15f3d60 --- /dev/null +++ b/runners/nrf52840dk/Makefile @@ -0,0 +1,148 @@ +.PHONY: build build-dk build-dk-nfc-only build-solo build-dk-test flash flash-jtag flash-jtag-test flash-set-key bacon clean-dfu + +# ── Paths ───────────────────────────────────────────────────────────────────── +ELF := ../../target/thumbv7em-none-eabihf/release/runner-nrf52840dk +APP_HEX := $(ELF).hex + +DFU_DIR := dfu +# Prefer prod artifacts if they exist (made by `make flash-set-key`), +# fall back to the committed dev key + bootloader. +DFU_KEY := $(if $(wildcard $(DFU_DIR)/prod.pem),$(DFU_DIR)/prod.pem,$(DFU_DIR)/debug.pem) +BOOT_HEX := $(if $(wildcard $(DFU_DIR)/bootloader_prod.hex),$(DFU_DIR)/bootloader_prod.hex,$(DFU_DIR)/bootloader.hex) +# MBR is a separate hex (~4 KB) — not bundled in the bootloader's own hex. +# Lives at 0x0; jumps to bootloader at UICR.NRFFW[0] = 0xF4000. +MBR_HEX := $(DFU_DIR)/mbr.hex + +# nrfutil DFU port — override on cmdline if auto-detect picks the wrong one. +# The DK exposes two tty.usbmodem* ports for J-Link OB's own VCP (serial +# starts with `001050...` on PCA10056). Filter those out so we hit the +# chip's USB CDC — that's the bootloader (VID 0x1915 PID 0x521F) when in +# DFU mode, or the app's CDC when in app mode. +NRF_PORT ?= $(shell ls /dev/tty.usbmodem* 2>/dev/null | grep -v "tty.usbmodem001050" | head -1) + +# Nordic's Python nrfutil — `pip3 install --user --break-system-packages nrfutil`. +# Adafruit's fork dropped the `pkg`/`settings` subcommands we need; use upstream. +# nrfutil 5.2.0 is the last working version; 6.x has dependencies (pc-ble-driver-py) +# without wheels for modern Python. On Python 3.14 you also need the patches in +# dfu/README.md applied to nordicsemi/dfu/signing.py. +PATH := $(HOME)/Library/Python/3.14/bin:$(PATH) +NRFUTIL := nrfutil + +APP_VER ?= 1 +HW_VER := 52 + +# ── Build targets ───────────────────────────────────────────────────────────── +# `build` builds whatever the default Cargo features pick (= board-dk). +# `build-dk` and `build-solo` force a specific board; subsequent flash targets +# are board-agnostic — they flash whatever's currently in $(ELF). +build: build-dk + +# Default DK build: CTAPHID + USB-CCID + NFC. CCID is on by default — phones +# do FIDO2-over-NFC (incl. registration) fine with it; only strict bench +# readers (ACR1252) go flaky from the USBD/NFCT EasyDMA contention. +build-dk: + cargo build --release --no-default-features --features board-dk,ccid + +# NFC-only DK build (no USB-CCID): max NFC reliability, solid on strict readers +# too. Use when you need contactless to be rock-solid and don't need contact-CCID. +build-dk-nfc-only: + cargo build --release --no-default-features --features board-dk + +build-solo: + cargo build --release --no-default-features --features board-solo + +# Builds with `test-up-control` on, which adds the `UP_CONTROL` static +# the host-side test harness writes via probe-rs to drive user-presence +# approvals. Otherwise identical to `build-dk` (CCID on). NEVER ship this build. +build-dk-test: + cargo build --release --no-default-features --features board-dk,ccid,test-up-control,no-reset-time-window + +# ── DFU upgrade (the common case) ───────────────────────────────────────────── +# Assumes bootloader is already installed via `make flash-jtag`. App must be +# in DFU mode — either via admin "enter DFU" command, or hold Button 4 at +# reset to enter via the bootloader's built-in trigger. +flash: + @if [ ! -f "$(ELF)" ]; then echo "ERROR: no ELF at $(ELF) — run 'make build-dk' or 'make build-solo' first"; exit 1; fi + arm-none-eabi-objcopy -O ihex $(ELF) $(APP_HEX) + $(NRFUTIL) pkg generate \ + --hw-version $(HW_VER) \ + --sd-req 0 \ + --application-version $(APP_VER) \ + --application $(APP_HEX) \ + --key-file $(DFU_KEY) \ + /tmp/firmware.zip + @if [ -z "$(NRF_PORT)" ]; then \ + echo "ERROR: no /dev/tty.usbmodem* found. Is the device in DFU mode?"; \ + exit 1; \ + fi + @echo "Using port $(NRF_PORT) (override with NRF_PORT=...)" + $(NRFUTIL) dfu usb-serial -pkg /tmp/firmware.zip -p $(NRF_PORT) -b 115200 + +# ── Initial install via JLink (one-shot per device, also for recovery) ──────── +# The default build runs CTAPHID + CCID over USB and NFC together — every app +# over both transports. No coexistence tradeoff on the nRF52840. +flash-jtag: $(BOOT_HEX) + @if [ ! -f "$(ELF)" ]; then echo "ERROR: no ELF at $(ELF) — run 'make build-dk' or 'make build-solo' first"; exit 1; fi + cp $(ELF) $(ELF).elf + arm-none-eabi-objcopy -O ihex $(ELF) $(APP_HEX) + $(NRFUTIL) settings generate \ + --family NRF52840 \ + --application $(APP_HEX) \ + --application-version $(APP_VER) \ + --bootloader-version 1 \ + --bl-settings-version 2 \ + /tmp/settings.hex + @printf 'r\nhalt\nerase\nloadfile %s\nloadfile %s\nloadfile %s\nloadfile /tmp/settings.hex\nr\ng\nq\n' \ + "$(MBR_HEX)" "$(BOOT_HEX)" "$(APP_HEX)" > /tmp/flash-jtag.jlink + JLinkExe -device nRF52840_xxAA -if SWD -speed 4000 -autoconnect 1 \ + -CommanderScript /tmp/flash-jtag.jlink + +# Build the test-up-control variant + flash via J-Link. The companion +# host-side tests (see `runners/pc/scripts/device_test.sh`) drive UP via +# `probe-rs write` against `UP_CONTROL`. NEVER ship. +flash-jtag-test: build-dk-test + $(MAKE) flash-jtag + +# ── Generate a fresh prod keypair + rebuild bootloader to trust it ──────────── +# Run once. Output: dfu/prod.pem + dfu/bootloader_prod.hex (both gitignored). +# Subsequent `make flash` and `make flash-jtag` automatically prefer these. +# Requires NRF5_SDK_ROOT to point at an extracted nRF5 SDK 17.1.0. +flash-set-key: + @if [ -z "$(NRF5_SDK_ROOT)" ]; then \ + echo "ERROR: set NRF5_SDK_ROOT to your nRF5 SDK 17.1.0 directory"; \ + exit 1; \ + fi + $(NRFUTIL) keys generate $(DFU_DIR)/prod.pem + @echo "Wrote $(DFU_DIR)/prod.pem" + $(NRFUTIL) keys display --key pk --format code --out_file $(DFU_DIR)/dfu_public_key.c $(DFU_DIR)/prod.pem + @echo "Wrote $(DFU_DIR)/dfu_public_key.c" + # Backup SDK pubkey + linker, swap in ours, build with the same tighter + # layout as bootloader.hex (origin 0xF4000, length 0xA000), restore after. + cp $(NRF5_SDK_ROOT)/examples/dfu/dfu_public_key.c $(NRF5_SDK_ROOT)/examples/dfu/dfu_public_key.c.bak + cp $(DFU_DIR)/dfu_public_key.c $(NRF5_SDK_ROOT)/examples/dfu/dfu_public_key.c + cp $(NRF5_SDK_ROOT)/examples/dfu/open_bootloader/pca10056_usb/armgcc/open_bootloader_gcc_nrf52.ld \ + $(NRF5_SDK_ROOT)/examples/dfu/open_bootloader/pca10056_usb/armgcc/open_bootloader_gcc_nrf52.ld.bak + sed -i.bak 's|FLASH (rx) : ORIGIN = 0xe0000, LENGTH = 0x1e000|FLASH (rx) : ORIGIN = 0xf4000, LENGTH = 0xa000|' \ + $(NRF5_SDK_ROOT)/examples/dfu/open_bootloader/pca10056_usb/armgcc/open_bootloader_gcc_nrf52.ld + # nRF5 SDK was tested with GCC ~9; modern (15+) is stricter. Disable -Werror. + sed -i.werror.bak 's/CFLAGS += -Wall -Werror/CFLAGS += -Wall/' \ + $(NRF5_SDK_ROOT)/examples/dfu/open_bootloader/pca10056_usb/armgcc/Makefile + cd $(NRF5_SDK_ROOT)/examples/dfu/open_bootloader/pca10056_usb/armgcc && \ + $(MAKE) clean GNU_INSTALL_ROOT=/opt/homebrew/bin/ GNU_VERSION=15.2.1 GNU_PREFIX=arm-none-eabi && \ + $(MAKE) GNU_INSTALL_ROOT=/opt/homebrew/bin/ GNU_VERSION=15.2.1 GNU_PREFIX=arm-none-eabi + cp $(NRF5_SDK_ROOT)/examples/dfu/open_bootloader/pca10056_usb/armgcc/_build/nrf52840_xxaa.hex \ + $(DFU_DIR)/bootloader_prod.hex + # Restore SDK to original. + mv $(NRF5_SDK_ROOT)/examples/dfu/dfu_public_key.c.bak $(NRF5_SDK_ROOT)/examples/dfu/dfu_public_key.c + mv $(NRF5_SDK_ROOT)/examples/dfu/open_bootloader/pca10056_usb/armgcc/open_bootloader_gcc_nrf52.ld.bak \ + $(NRF5_SDK_ROOT)/examples/dfu/open_bootloader/pca10056_usb/armgcc/open_bootloader_gcc_nrf52.ld + mv $(NRF5_SDK_ROOT)/examples/dfu/open_bootloader/pca10056_usb/armgcc/Makefile.werror.bak \ + $(NRF5_SDK_ROOT)/examples/dfu/open_bootloader/pca10056_usb/armgcc/Makefile + @echo "Done. Now run 'make flash-jtag' to install the prod bootloader + app." + +clean-dfu: + rm -f $(DFU_DIR)/prod.pem $(DFU_DIR)/bootloader_prod.hex + @echo "Removed prod artifacts. Subsequent flashes use the committed debug key." + +bacon: + bacon diff --git a/runners/nrf52840dk/README.md b/runners/nrf52840dk/README.md new file mode 100644 index 0000000..0011964 --- /dev/null +++ b/runners/nrf52840dk/README.md @@ -0,0 +1,91 @@ +# solo2 on Nordic nRF52840-DK + +### the first open source FIDO2 security key (on nRF, with NFC) + +![nRF52840-DK with NFC antenna](https://www.crowdsupply.com/img/6714/d2036608-2b4c-4216-bd24-0e0cde406714/nrf52840-dk-with-nfc-antenna_png_md-fixed-xl.jpg) + +> ⚠️ **Experimental — for developers only.** +> SoloKeys does not produce or sell any device using the Nordic nRF52840 chip. +> This runner is a research port of the solo2 firmware to the +> [nRF52840 Development Kit](https://www.nordicsemi.com/Products/Development-hardware/nRF52840-DK) +> for hacking, evaluation, and learning. **Do not** use it as a real +> security key — there is no provisioning, no attestation, no hardware +> root of trust, and no security review. + +## What works + +- USB CTAPHID — FIDO2 + U2F over USB (`fido-authenticator`) +- NFC — FIDO2 + U2F over NFC, plus an NDEF tag pointing at solokeys.com + (a URL banner phones with a background NFC scanner can pop on tap) +- Storage — internal NVMC at `0x000C_0000` (256 KiB) for trussed-managed + credentials; volatile RAM-backed for everything else + +Tested with [webauthn.io](https://webauthn.io) (registration and +authentication) on the Nordic nRF52840-DK board. + +## Build + +``` +make build-nrf +``` + +(or `cargo build --release` from this directory — cargo needs the +runner's `.cargo/config.toml` to pick up `target = thumbv7em-none-eabihf`, +so building with `-p` from the workspace root will fail.) + +The output ELF lands at `target/thumbv7em-none-eabihf/release/runner-nrf52840dk` +(no `.elf` extension — cargo names the binary after the package). + +## Flash + +You need a J-Link probe (the DK has one onboard) and `JLinkExe`. + +``` +cd runners/nrf52840dk +cp ../../target/thumbv7em-none-eabihf/release/runner-nrf52840dk \ + ../../target/thumbv7em-none-eabihf/release/runner-nrf52840dk.elf +JLinkExe -device nRF52840_xxAA -if SWD -speed 4000 -autoconnect 1 \ + -CommandFile flash.jlink +``` + +The script halts the chip, loads the ELF, resets, and runs. + +## Live RTT logs + +``` +JLinkRTTLogger -Device nRF52840_xxAA -If SWD -Speed 4000 -RTTChannel 0 out.log +defmt-print -e ../../target/thumbv7em-none-eabihf/release/runner-nrf52840dk.elf < out.log +``` + +`DEFMT_LOG=info` is set in `.cargo/config.toml`; raise to `debug` for +more chatter. + +## Test + +- USB: plug the J2 (USB) connector into a host. Open + [webauthn.io](https://webauthn.io), pick "Register", confirm with + SW1 or SW2 (any button → user-presence). +- NFC: tap the antenna pad on the DK with an NFC-capable phone. A phone + running a background NFC scanner should pop a "https://solokeys.com/" + banner. For WebAuthn over NFC, open webauthn.io in a browser that + supports NFC security keys and choose "Security key" / "NFC" when + prompted. + +## NFC stack and the library + +The NFC implementation depends on Nordic's **`libnfc_t4t.a`** +(Type 4 Tag library, distributed in [nRF Connect SDK]) — a +precompiled, **closed-source** binary blob that handles the ISO 14443-4 +ISO-DEP layer (anti-collision, RATS/ATS, I-block framing, chaining, WTX). +The blob, the chip-layer `libnrfx_nfct.a`, and the platform glue are +all in [`components/nrf-nfc/`](../../components/nrf-nfc/); see that +crate's `vendor/README.md` for source/version provenance. The runner's +`nfct.rs` is a thin Rust wrapper that delivers reassembled APDUs to +`apdu-dispatch` and ships responses back through the library. + +If you want a fully open NFC stack, the workspace also has an +open-source ISO-DEP layer in +[`components/nfc-device`](../../components/nfc-device). It isn't wired +into this runner today. + +[nRF Connect SDK]: https://www.nordicsemi.com/Products/Development-software/nRF-Connect-SDK diff --git a/runners/nrf52840dk/build.rs b/runners/nrf52840dk/build.rs new file mode 100644 index 0000000..f26302f --- /dev/null +++ b/runners/nrf52840dk/build.rs @@ -0,0 +1,15 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + let out = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("memory.x")) + .unwrap() + .write_all(include_bytes!("memory.x")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + println!("cargo:rerun-if-changed=memory.x"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/runners/nrf52840dk/dfu/.gitignore b/runners/nrf52840dk/dfu/.gitignore new file mode 100644 index 0000000..80d42b7 --- /dev/null +++ b/runners/nrf52840dk/dfu/.gitignore @@ -0,0 +1,2 @@ +prod.pem +bootloader_prod.hex diff --git a/runners/nrf52840dk/dfu/README.md b/runners/nrf52840dk/dfu/README.md new file mode 100644 index 0000000..f714368 --- /dev/null +++ b/runners/nrf52840dk/dfu/README.md @@ -0,0 +1,188 @@ +# DFU artifacts + +Files in this directory back the `make flash` and `make flash-jtag` targets. + +## One-time host setup + +The Makefile uses Nordic's Python `nrfutil` (NOT the `adafruit-nrfutil` fork — +adafruit dropped the `pkg`/`settings` subcommands we need). + +```sh +pip3 install --user --break-system-packages nrfutil +``` + +The latest `nrfutil` that installs on modern Python is **5.2.0**; 6.x has a +native dep (`pc-ble-driver-py`) without wheels for current Python/arm64. + +### Python 3.14 patches (skip on 3.12 and earlier) + +`nrfutil 5.2.0` ships a lot of Python-2-era code that breaks on 3.14. The +sweep below covers everything the `make flash-jtag` + `make flash` paths +hit in practice. Run all of these against your venv's `nordicsemi/` +(replace `$VENV` with your venv path, e.g. `solo2/venv`): + +```sh +NS=$VENV/lib/python3.14/site-packages/nordicsemi + +# xrange → range, .iteritems() → .items() and friends, .tostring() → .tobytes() +find $NS -name "*.py" -exec sed -i '' \ + -e 's/xrange/range/g' \ + -e 's/\.iteritems()/.items()/g' \ + -e 's/\.iterkeys()/.keys()/g' \ + -e 's/\.itervalues()/.values()/g' \ + -e 's/\.tostring()/.tobytes()/g' \ + {} \; + +# signing.py: pem write mode + hex/bytes confusion +F=$NS/dfu/signing.py +sed -i '' \ + -e 's|with open(filename, "w") as sk_file:|with open(filename, "wb") as sk_file:|' \ + -e "s|sk_hex = \"\".join(c.encode('hex') for c in self.sk.to_string())|sk_hex = self.sk.to_string().hex()|" \ + -e 's|sk_hexlify = binascii.hexlify(self.sk.to_string())|sk_hexlify = binascii.hexlify(self.sk.to_string()).decode()|' \ + -e 's|vk_hexlify = binascii.hexlify(vk.to_string())|vk_hexlify = binascii.hexlify(vk.to_string()).decode()|' \ + -e 's|vk_hex = binascii.hexlify(vk.to_string())|vk_hex = binascii.hexlify(vk.to_string()).decode()|' \ + "$F" + +# intelhex/__init__.py: int division, removed (int, long), bytes-to-str confusion, +# dict.keys() not having .sort() +F=$NS/dfu/intelhex/__init__.py +sed -i '' \ + -e 's/(int, long)/(int,)/g' \ + -e 's|return asstr(self._tobinarray_really(start, end, pad, size).tobytes())|return self._tobinarray_really(start, end, pad, size).tobytes()|' \ + -e 's|addresses = self._buf.keys()|addresses = sorted(self._buf.keys())|g' \ + -e 's|^\([[:space:]]*\)addresses\.sort()$|\1pass # already sorted|g' \ + "$F" + +# nrfhex.py: int division producing float +F=$NS/dfu/nrfhex.py +sed -i '' 's|(size + (word_size - 1)) / word_size|(size + (word_size - 1)) // word_size|' "$F" + +# intelhex/compat.py: asbytes() rejecting bytearray +F=$NS/dfu/intelhex/compat.py +sed -i '' 's|if isinstance(s, bytes):|if isinstance(s, (bytes, bytearray)):|' "$F" +# (also wrap return as bytes(s) — safer for bytearray input) + +# init_packet_pb.py: protobuf bytes field rejecting str +F=$NS/dfu/init_packet_pb.py +# (manual: wrap boot_validation_bytes[i] in `if isinstance(_bv, str): _bv = _bv.encode('latin1')`) + +# dfu_transport_serial.py: map() not iterable + bytes already int + int division +F=$NS/dfu/dfu_transport_serial.py +sed -i '' \ + -e 's|+ map(ord, struct.pack(.>, + _led3: Pin>, +} + +impl Leds { + pub fn set_brightness(&mut self, b: u8) { + let _ = if b >= 128 { + self.led4.set_low() // active-low: LOW = on + } else { + self.led4.set_high() // HIGH = off + }; + } +} + +pub struct Buttons { + pub btn1: Pin>, + pub btn2: Pin>, + pub btn3: Pin>, + pub touch1: CapTouchPad, + pub touch2: CapTouchPad, +} + +impl Buttons { + /// "Left" approve source: Btn1 (P0.11) OR cap-touch1 (P0.03). + pub fn left(&self) -> bool { + self.btn1.is_low().unwrap_or(false) || self.touch1.is_touched() + } + /// "Right" approve source: Btn2 (P0.12) OR cap-touch2 (P0.04). + pub fn right(&self) -> bool { + self.btn2.is_low().unwrap_or(false) || self.touch2.is_touched() + } + /// Explicit deny shortcut: Btn3 (P0.24). + pub fn explicit_deny(&self) -> bool { + self.btn3.is_low().unwrap_or(false) + } +} + +/// Take ownership of the P0 GPIO bank, configure pins, calibrate cap-touch +/// baselines (untouched). Returns Leds + Buttons. +/// +/// Cap-touch pins (P0.03, P0.04) are NOT claimed via the HAL — the driver +/// pokes `PIN_CNF[N]` directly on every measurement. Don't also `into_*` +/// them here or the HAL type-state will fight the driver. +pub fn init(p0_periph: P0) -> (Leds, Buttons) { + let parts = p0::Parts::new(p0_periph); + + // LED1, LED2 parked off (drop their handles — pins stay HIGH = off). + let _ = parts.p0_13.into_push_pull_output(Level::High).degrade(); + let _ = parts.p0_14.into_push_pull_output(Level::High).degrade(); + + let leds = Leds { + // LED3 owned but never touched again — pin held LOW = always on. + _led3: parts.p0_15.into_push_pull_output(Level::Low).degrade(), + led4: parts.p0_16.into_push_pull_output(Level::High).degrade(), + }; + + let mut touch1 = CapTouchPad::new(3); + let mut touch2 = CapTouchPad::new(4); + // Calibrate untouched. 64 samples × ~50 µs ≈ 3 ms — boot delay imperceptible. + touch1.calibrate(64); + touch2.calibrate(64); + + let buttons = Buttons { + btn1: parts.p0_11.into_pullup_input().degrade(), + btn2: parts.p0_12.into_pullup_input().degrade(), + btn3: parts.p0_24.into_pullup_input().degrade(), + touch1, + touch2, + }; + + (leds, buttons) +} diff --git a/runners/nrf52840dk/src/board/mod.rs b/runners/nrf52840dk/src/board/mod.rs new file mode 100644 index 0000000..ba76459 --- /dev/null +++ b/runners/nrf52840dk/src/board/mod.rs @@ -0,0 +1,249 @@ +//! Board glue: trussed UserInterface + Syscall (shared); per-board pin +//! defs + HAL handles live in [`dk`] / [`solo`] (compiled in via the +//! `board-dk` / `board-solo` Cargo feature, mutually exclusive). +//! +//! Iteration 1 — minimum viable behaviour: +//! * boot: LED4 blinks 3 × at startup (sync, in `LedController::new`). +//! * idle: LED off. +//! * waiting for user presence: LED on (solid). +//! * any button (Btn1/Btn2/Btn3) approves; no deny / no cap-touch yet. + +#[cfg(feature = "board-dk")] +pub mod dk; +#[cfg(feature = "board-dk")] +pub use dk::{init, Buttons, Leds}; + +#[cfg(feature = "board-solo")] +pub mod solo; +#[cfg(feature = "board-solo")] +pub use solo::{init, Buttons, Leds}; + +#[cfg(all(feature = "board-dk", feature = "board-solo"))] +compile_error!("`board-dk` and `board-solo` are mutually exclusive — pick one"); + +#[cfg(not(any(feature = "board-dk", feature = "board-solo")))] +compile_error!("enable one of `board-dk` / `board-solo` (default = `board-dk`)"); + +use core::time::Duration; +use cortex_m::peripheral::SCB; +use trussed::platform::{consent, reboot, ui}; + +#[derive(Default)] +pub struct Syscall; + +impl trussed::client::Syscall for Syscall { + #[inline] + fn syscall(&mut self) { + rtic::pend(nrf52840_pac::Interrupt::SWI0_EGU0); + } +} + +// ── Gesture detector (shared, board-agnostic) ──────────────────────────────── +// +// Per-board `Buttons` exposes three primitive signals — left, right, +// explicit_deny. The detector commits a gesture on RELEASE so it can +// distinguish "single press → approve" from "both held → deny". +// Explicit-deny fires immediately on press, debounced one-shot. + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum Gesture { + None, + Approve, + Deny, +} + +const DEBOUNCE_MS: u32 = 30; + +pub struct GestureDetector { + pending_started_ms: Option, + both_seen: bool, + last_explicit_deny: bool, +} + +impl Default for GestureDetector { + fn default() -> Self { + Self::new() + } +} + +impl GestureDetector { + pub const fn new() -> Self { + Self { + pending_started_ms: None, + both_seen: false, + last_explicit_deny: false, + } + } + + pub fn poll(&mut self, left: bool, right: bool, explicit_deny: bool, now_ms: u32) -> Gesture { + if explicit_deny && !self.last_explicit_deny { + self.last_explicit_deny = true; + self.pending_started_ms = None; + self.both_seen = false; + return Gesture::Deny; + } + if !explicit_deny { + self.last_explicit_deny = false; + } + + match (left, right) { + (false, false) => { + let result = match self.pending_started_ms { + Some(start) if now_ms.wrapping_sub(start) >= DEBOUNCE_MS => { + if self.both_seen { + Gesture::Deny + } else { + Gesture::Approve + } + } + _ => Gesture::None, + }; + self.pending_started_ms = None; + self.both_seen = false; + result + } + (l, r) => { + if self.pending_started_ms.is_none() { + self.pending_started_ms = Some(now_ms); + } + if l && r { + self.both_seen = true; + } + Gesture::None + } + } + } +} + +fn now_ms() -> u32 { + use rtic_monotonics::Monotonic; + crate::app::Mono::now().duration_since_epoch().to_millis() +} + +// ── Test hook: probe-rs-driven user-presence override ─────────────────────── +// +// Value semantics match `runners/pc/tests/support/up.rs`: +// 0 = no override (fall through to real button polling) +// 1 = approve once (Normal), consumed after one read +// 2 = approve once (Strong), consumed after one read +// 128 = deny sticky (every check returns None until reset to 0) +// 129 = approve sticky (every check returns Normal until reset to 0) +// +// Placed in `.uninit` with `no_mangle` so its address is stable and +// discoverable via the ELF symbol table. NEVER enable this feature on +// firmware shipped to users — it bypasses the real consent gate. +#[cfg(feature = "test-up-control")] +#[unsafe(link_section = ".uninit")] +#[unsafe(no_mangle)] +pub static mut UP_CONTROL: u8 = 0; + +// ── Trussed UserInterface ──────────────────────────────────────────────────── + +pub struct UserInterface { + leds: Leds, + buttons: Buttons, + gesture: GestureDetector, + status: ui::Status, +} + +impl UserInterface { + /// Construct + run the boot indication: 3 quick LED blinks (sync). + pub fn new(mut leds: Leds, buttons: Buttons) -> Self { + const BLINK_CYCLES: u32 = 64_000_000 / 5 / 4; // ~200 ms at 64 MHz + for _ in 0..3 { + leds.set_brightness(255); + cortex_m::asm::delay(BLINK_CYCLES); + leds.set_brightness(0); + cortex_m::asm::delay(BLINK_CYCLES); + } + Self { + leds, + buttons, + gesture: GestureDetector::new(), + status: ui::Status::Idle, + } + } +} + +impl trussed::platform::UserInterface for UserInterface { + fn check_user_presence(&mut self) -> consent::Level { + // Probe-rs UP override: write `UP_CONTROL` from the host (via + // probe-rs) to drive automated tests. One-shot values (1, 2) are + // consumed; sticky (128, 129) persist until the host resets to 0. + #[cfg(feature = "test-up-control")] + { + let val = unsafe { core::ptr::read_volatile(&raw const UP_CONTROL) }; + match val { + 0 => { + // No override — fall through. + } + 1 => { + unsafe { core::ptr::write_volatile(&raw mut UP_CONTROL, 0) }; + return consent::Level::Normal; + } + 2 => { + unsafe { core::ptr::write_volatile(&raw mut UP_CONTROL, 0) }; + return consent::Level::Strong; + } + 128 => return consent::Level::None, + 129 => return consent::Level::Normal, + // Strong sticky — required to drive CTAP 2.3 long-touch + // Reset from a host that can't pulse the UP byte per + // call (the only consent level that satisfies both + // `user_present` and `user_present_strong`). + 130 => return consent::Level::Strong, + _ => return consent::Level::None, + } + } + + // NFC: tap = consent (no button to press). + if crate::nfct::field_on() { + return consent::Level::Normal; + } + match self.gesture.poll( + self.buttons.left(), + self.buttons.right(), + self.buttons.explicit_deny(), + now_ms(), + ) { + Gesture::Approve => consent::Level::Normal, + Gesture::Deny => { + // ctaphid-dispatch sets the active app's InterruptFlag + // to Working before calling us, so the CAS in interrupt() + // succeeds and trussed's UP loop bails on the next iter. + crate::types::interrupt_all_apps(); + consent::Level::None + } + Gesture::None => consent::Level::None, + } + } + + fn set_status(&mut self, status: ui::Status) { + if self.status != status { + self.status = status; + let now_up = matches!(status, ui::Status::WaitingForUserPresence); + self.leds.set_brightness(if now_up { 255 } else { 0 }); + } + } + + fn status(&self) -> ui::Status { + self.status + } + + fn refresh(&mut self) {} + + fn uptime(&mut self) -> Duration { + // Read RTIC SysTick monotonic — 64-bit ms count, doesn't wrap. + // Was DWT cycle counter (32-bit, wraps at ~67 s). Trussed's UP loop + // computes `nowtime - starttime` which panicked on wrap. + use rtic_monotonics::Monotonic; + let now = crate::app::Mono::now(); + Duration::from_millis(u64::from(now.duration_since_epoch().to_millis())) + } + + fn reboot(&mut self, _to: reboot::To) -> ! { + SCB::sys_reset() + } + + fn wink(&mut self, _: Duration) {} +} diff --git a/runners/nrf52840dk/src/board/solo.rs b/runners/nrf52840dk/src/board/solo.rs new file mode 100644 index 0000000..e58bfd5 --- /dev/null +++ b/runners/nrf52840dk/src/board/solo.rs @@ -0,0 +1,71 @@ +//! SoloKeys USB pin definitions + board-specific HAL. +//! +//! Pinout (from the Solo Micro USB schematic): +//! LED = P0.03 (single, active-high; PWM-driven) +//! Cap-touch1 = P0.02 (J1, AIN0) +//! Cap-touch2 = P0.31 (J2, AIN7) +//! NFC1 = P0.09 (dedicated NFC pin) +//! NFC2 = P0.10 (dedicated NFC pin) +//! SPI to TROPIC01: +//! SCLK = P0.13 +//! MOSI/SDO = P0.15 (host out → TROPIC SDI) +//! MISO/SDI = P0.17 (host in ← TROPIC SDO) +//! CSn = P0.14 +//! GPO = P0.16 (TROPIC notify out → nRF input) +//! nRESET = P0.18 (shared with SWD reset) + +use crate::cap_touch::CapTouchPad; +use embedded_hal::digital::v2::OutputPin; +use nrf52840_hal::gpio::{p0, Level, Output, Pin, PushPull}; +use nrf52840_pac::P0; + +/// Single-LED driver for the Solo's P0.03 LED. Active-high: pin HIGH = on. +/// Software-toggled — see board/dk.rs for why we dropped hardware PWM. +pub struct Leds { + led: Pin>, +} + +impl Leds { + pub fn set_brightness(&mut self, b: u8) { + let _ = if b >= 128 { + self.led.set_high() // active-high: HIGH = on + } else { + self.led.set_low() // LOW = off + }; + } +} + +pub struct Buttons { + touch1: CapTouchPad, + touch2: CapTouchPad, +} + +impl Buttons { + pub fn left(&self) -> bool { + self.touch1.is_touched() + } + pub fn right(&self) -> bool { + self.touch2.is_touched() + } + /// No explicit-deny on Solo — use both-touch. + pub fn explicit_deny(&self) -> bool { + false + } +} + +/// Cap-touch pins (P0.02, P0.31) are NOT claimed via the HAL — the driver +/// pokes `PIN_CNF[N]` directly. Calibrates baselines untouched. +pub fn init(p0_periph: P0) -> (Leds, Buttons) { + let parts = p0::Parts::new(p0_periph); + + let leds = Leds { + led: parts.p0_03.into_push_pull_output(Level::Low).degrade(), + }; + + let mut touch1 = CapTouchPad::new(2); + let mut touch2 = CapTouchPad::new(31); + touch1.calibrate(64); + touch2.calibrate(64); + + (leds, Buttons { touch1, touch2 }) +} diff --git a/runners/nrf52840dk/src/cap_touch.rs b/runners/nrf52840dk/src/cap_touch.rs new file mode 100644 index 0000000..9fea9e8 --- /dev/null +++ b/runners/nrf52840dk/src/cap_touch.rs @@ -0,0 +1,120 @@ +//! Polled capacitive-touch driver, board-agnostic. +//! +//! Hardware: any GPIO with the relaxation-oscillator circuit +//! GPIO ── 20pF series cap ── pad ; pad-to-GND via 1MΩ. +//! Used on the SoloKeys USB board (built-in pads on P0.02 + P0.31) +//! and on the nRF52840-DK (manually wired pads on P0.03 + P0.04 — see +//! board/dk.rs for the wiring diagram). +//! +//! Algorithm (per scan): +//! 1. Configure pin as output, drive HIGH → charges the pad cap. +//! 2. Wait long enough to saturate (~25 µs at 64 MHz). +//! 3. Switch pin to input (no pull) → cap discharges through 1 MΩ. +//! 4. Spin-loop counting iterations until the input reads LOW. +//! 5. Touch increases pad capacitance (finger ~5–20 pF) → longer +//! discharge time → higher loop count. +//! +//! Calibration: take a baseline at boot (untouched). At runtime, a +//! reading > `baseline * threshold_pct / 100` counts as touched. +//! +//! CPU cost: ~50–100 µs per scan. Run at ~50 ms cadence → ~0.2 % CPU +//! per pad, negligible. No peripheral allocated — direct PIN_CNF pokes +//! on the P0 base register are portable across all nRF52840 boards. +//! +//! All pads on the nRF52840 P0 register block are supported. P1.x pads +//! would need a sister struct pointing at P1::ptr(). + +use nrf52840_pac::P0; + +pub struct CapTouchPad { + pin: u8, // GPIO bit position 0..32 within P0 + baseline: u32, // calibrated discharge count when untouched + threshold_pct: u32, // touched if reading > baseline * pct / 100 +} + +const DEFAULT_THRESHOLD_PCT: u32 = 130; +const CHARGE_CYCLES: u32 = 2_000; // ~31 µs at 64 MHz +const MAX_LOOP_COUNT: u32 = 50_000; // hard ceiling per scan (~ms-range) + +impl CapTouchPad { + /// Construct a pad driver bound to a P0 GPIO bit. The caller must + /// guarantee the pin isn't otherwise driven by the HAL — this driver + /// reconfigures `PIN_CNF[N]` on every scan. + pub fn new(pin: u8) -> Self { + debug_assert!(pin < 32); + Self { + pin, + baseline: 0, + threshold_pct: DEFAULT_THRESHOLD_PCT, + } + } + + /// Take `samples` measurements and use the average as the + /// untouched baseline. Call once at boot, untouched. + pub fn calibrate(&mut self, samples: u32) { + debug_assert!(samples > 0); + let mut sum = 0u64; + for _ in 0..samples { + sum += self.measure() as u64; + } + self.baseline = (sum / samples as u64) as u32; + } + + /// Override the touch threshold (default 130 = 30 % above baseline). + /// Lower → more sensitive (more false positives); higher → less + /// sensitive (might miss light touches). + #[allow(dead_code)] + pub fn set_threshold_pct(&mut self, pct: u32) { + self.threshold_pct = pct; + } + + #[allow(dead_code)] + pub fn baseline(&self) -> u32 { + self.baseline + } + + /// `true` if the most recent measurement exceeds the touch threshold. + pub fn is_touched(&self) -> bool { + self.measure() > self.baseline.saturating_mul(self.threshold_pct) / 100 + } + + /// Raw discharge-count measurement. Useful for tuning + diagnostics. + pub fn measure(&self) -> u32 { + let p0 = unsafe { &*P0::ptr() }; + let bit = self.pin as usize; + let mask = 1u32 << self.pin; + + // Charge: drive output HIGH. + p0.pin_cnf[bit].write(|w| { + w.dir().output(); + w.input().disconnect(); + w.pull().disabled(); + w.drive().s0s1(); + w.sense().disabled(); + w + }); + p0.outset.write(|w| unsafe { w.bits(mask) }); + cortex_m::asm::delay(CHARGE_CYCLES); + + // Switch to input, no pull. + p0.pin_cnf[bit].write(|w| { + w.dir().input(); + w.input().connect(); + w.pull().disabled(); + w.drive().s0s1(); + w.sense().disabled(); + w + }); + + // Spin until LOW; cap discharges through the external 1 MΩ. + let in_reg = &p0.in_; + let mut count = 0u32; + while count < MAX_LOOP_COUNT { + if in_reg.read().bits() & mask == 0 { + break; + } + count = count.wrapping_add(1); + } + count + } +} diff --git a/runners/nrf52840dk/src/dispatch.rs b/runners/nrf52840dk/src/dispatch.rs new file mode 100644 index 0000000..2463c57 --- /dev/null +++ b/runners/nrf52840dk/src/dispatch.rs @@ -0,0 +1,3 @@ +//! Trussed extension dispatch — shared with the lpc55 runner via `solo-apps`. + +pub use solo_apps::dispatch::*; diff --git a/runners/nrf52840dk/src/flash.rs b/runners/nrf52840dk/src/flash.rs new file mode 100644 index 0000000..9feddbc --- /dev/null +++ b/runners/nrf52840dk/src/flash.rs @@ -0,0 +1,74 @@ +//! NVMC-backed littlefs2 0.7 storage for the nRF52840 internal flash region. +//! +//! Region: 0x000A_4000..0x000F_4000 (320 KiB = 80 × 4 KiB pages). Sits +//! between the app (ends at 0xA4000) and the Nordic Open Bootloader +//! (starts at 0xF4000). +//! Reads bypass `Nvmc` via memory-mapped flash (no state mutation needed). +//! Writes/erases go through `Nvmc` (handles NVMC.READY waits + page semantics). + +use embedded_storage::nor_flash::NorFlash; +use generic_array::typenum::{U1, U256}; +use nrf52840_hal::nvmc::Nvmc; +use nrf52840_pac::NVMC; + +use littlefs2::driver::Storage; +use littlefs2::io::{Error as LfsError, Result as LfsResult}; + +pub const FILESYSTEM_BASE: usize = 0x000A_4000; +pub const FILESYSTEM_LEN: usize = 320 * 1024; +pub const PAGE_SIZE: usize = 4096; +pub const FS_BLOCK_COUNT: usize = FILESYSTEM_LEN / PAGE_SIZE; + +pub struct InternalFlashStorage { + nvmc: Nvmc, +} + +impl InternalFlashStorage { + pub fn new(nvmc: NVMC) -> Self { + let storage: &'static mut [u8] = + unsafe { core::slice::from_raw_parts_mut(FILESYSTEM_BASE as *mut u8, FILESYSTEM_LEN) }; + Self { + nvmc: Nvmc::new(nvmc, storage), + } + } +} + +impl Storage for InternalFlashStorage { + const READ_SIZE: usize = 4; + const WRITE_SIZE: usize = 4; + const BLOCK_SIZE: usize = PAGE_SIZE; + const BLOCK_COUNT: usize = FS_BLOCK_COUNT; + const BLOCK_CYCLES: isize = -1; + type CACHE_SIZE = U256; + type LOOKAHEAD_SIZE = U1; + + fn read(&mut self, off: usize, buf: &mut [u8]) -> LfsResult { + // Memory-mapped read. Equivalent to `self.nvmc.read(...)` but skips + // the NVMC.READY wait (reads don't need it). littlefs is supposed to + // keep off+len within BLOCK_COUNT * BLOCK_SIZE, but we re-check + // before constructing the raw slice — an out-of-bounds read here + // would land outside the dedicated FS region. + let end = off.checked_add(buf.len()).ok_or(LfsError::IO)?; + if end > FILESYSTEM_LEN { + return Err(LfsError::IO); + } + let src = + unsafe { core::slice::from_raw_parts((FILESYSTEM_BASE + off) as *const u8, buf.len()) }; + buf.copy_from_slice(src); + Ok(buf.len()) + } + + fn write(&mut self, off: usize, data: &[u8]) -> LfsResult { + self.nvmc + .write(off as u32, data) + .map_err(|_| LfsError::IO)?; + Ok(data.len()) + } + + fn erase(&mut self, off: usize, len: usize) -> LfsResult { + self.nvmc + .erase(off as u32, (off + len) as u32) + .map_err(|_| LfsError::IO)?; + Ok(len) + } +} diff --git a/runners/nrf52840dk/src/initializer.rs b/runners/nrf52840dk/src/initializer.rs new file mode 100644 index 0000000..208e31f --- /dev/null +++ b/runners/nrf52840dk/src/initializer.rs @@ -0,0 +1,296 @@ +//! Board bring-up for the nRF52840-DK runner. +//! +//! `init_board` constructs every hardware/driver object from the device +//! peripherals and returns them in [`BoardComponents`]. The RTIC `#[init]` +//! in `main.rs` keeps only what needs the RTIC context — the SysTick +//! monotonic, the keepalive channel, and the task spawns. Mirrors the +//! lpc55 runner's `runner::init_board` split. + +use core::fmt::Write; + +use apdu_dispatch::dispatch::ApduDispatch; +use apdu_dispatch::interchanges as apdu_interchanges; +use ctaphid_dispatch::{Channel as CtapChannel, DefaultDispatch as CtaphidDispatchDefault}; +use interchange::Channel; +use littlefs2::fs::{Allocation, Filesystem}; +use nrf52840_hal::{ + clocks::{Clocks, ExternalOscillator, Internal, LfOscStarted}, + rng::Rng, + usbd::{UsbPeripheral, Usbd}, +}; +use nrf52840_pac::POWER; +use static_cell::StaticCell; +use usb_device::{ + bus::UsbBusAllocator, + device::{UsbDevice, UsbDeviceBuilder, UsbVidPid}, +}; +#[cfg(feature = "ccid")] +use usbd_ccid::Ccid; +use usbd_ctaphid::CtapHid; + +use crate::dispatch::Dispatch; +use crate::flash::InternalFlashStorage; +use crate::nfct::NfctDevice; +use crate::types::{Apps, Board, RunnerStore, Trussed, VolatileStorage}; +use crate::CTAPHID_MESSAGE_SIZE; + +pub type UsbClock = Clocks; +pub type UsbBus = Usbd>; +pub type CtapHidClass = CtapHid<'static, 'static, 'static, UsbBus, CTAPHID_MESSAGE_SIZE>; +#[cfg(feature = "ccid")] +pub type CcidClass = Ccid<'static, 'static, UsbBus, { apdu_dispatch::interchanges::SIZE }>; +// CCID off: the contact smartcard interface is not enumerated, so the shared +// slot is a unit placeholder (never locked — its task accesses are cfg'd out). +#[cfg(not(feature = "ccid"))] +pub type CcidClass = (); + +/// Everything `init_board` builds from the device peripherals. The RTIC +/// `#[init]` splits these across the `Shared` / `Local` resource structs. +pub struct BoardComponents { + pub trussed: Trussed, + pub apps: Apps, + pub ctaphid_dispatch: CtaphidDispatchDefault<'static, 'static>, + pub apdu_dispatch: ApduDispatch<'static>, + pub nfc_apdu_rq: apdu_interchanges::Requester<'static>, + pub usbd: UsbDevice<'static, UsbBus>, + pub ctaphid: CtapHidClass, + pub ccid: CcidClass, + pub power: POWER, +} + +pub fn init_board(dp: nrf52840_pac::Peripherals) -> BoardComponents { + static CLOCKS: StaticCell = StaticCell::new(); + let clocks = CLOCKS.init(Clocks::new(dp.CLOCK).start_lfclk().enable_ext_hfosc()); + + // Post-mortem: read POWER.RESETREAS to learn why the chip last reset, + // then write-1-to-clear so the NEXT boot only sees the NEXT reset cause. + // Bit map (nRF52840 RM): bit 0 RESETPIN, 1 DOG (WDT), 2 SREQ + // (SCB::sys_reset / lockup post-reset), 3 LOCKUP, 16+ wake-from-System-OFF. + let reset_reas = dp.POWER.resetreas.read().bits(); + defmt::warn!("RESETREAS=0x{=u32:08x}", reset_reas); + dp.POWER.resetreas.write(|w| unsafe { w.bits(0xFFFFFFFF) }); + + // Power events for VBUS detection. + dp.POWER.intenset.write(|w| { + w.usbdetected() + .set_bit() + .usbpwrrdy() + .set_bit() + .usbremoved() + .set_bit() + }); + + // USBD interrupt sources (nrf-usbd 0.1 doesn't unmask these). + dp.USBD.intenset.write(|w| { + w.usbreset() + .set_bit() + .usbevent() + .set_bit() + .sof() + .set_bit() + .ep0datadone() + .set_bit() + .ep0setup() + .set_bit() + .endepin0() + .set_bit() + .endepout0() + .set_bit() + }); + + // Device UUID derived from FICR. Computed before the USB device is built + // so it can be exposed as the (hex-encoded) USB serial number; solo2-cli + // hex-decodes the serial to derive the device UUID and drops any device + // whose serial isn't valid hex. + let ficr = dp.FICR; + let did0 = ficr.deviceid[0].read().bits(); + let did1 = ficr.deviceid[1].read().bits(); + let mut uuid = [0u8; 16]; + uuid[0..4].copy_from_slice(&did0.to_be_bytes()); + uuid[4..8].copy_from_slice(&did1.to_be_bytes()); + + static SERIAL_STRING: StaticCell> = StaticCell::new(); + let serial_string = SERIAL_STRING.init(heapless::String::new()); + for b in &uuid { + write!(serial_string, "{:02x}", b).ok(); + } + let serial_number: &'static str = serial_string.as_str(); + + static USB_BUS: StaticCell> = StaticCell::new(); + let usb_bus = USB_BUS.init(Usbd::new(UsbPeripheral::new(dp.USBD, clocks))); + + static CTAP_CHANNEL: CtapChannel = Channel::new(); + let (ctaphid_rq, ctaphid_rp) = CTAP_CHANNEL.split().unwrap(); + + // APDU interchanges. NFC requester is fed by the t4t bridge in nfct.rs; + // USB requester is fed by the CCID class. + static NFC_APDU_CHANNEL: apdu_dispatch::interchanges::Channel = Channel::new(); + static USB_APDU_CHANNEL: apdu_dispatch::interchanges::Channel = Channel::new(); + let (nfc_apdu_rq, nfc_apdu_rp) = NFC_APDU_CHANNEL.split().unwrap(); + let (usb_apdu_rq, usb_apdu_rp) = USB_APDU_CHANNEL.split().unwrap(); + let apdu_dispatch = ApduDispatch::new(usb_apdu_rp, nfc_apdu_rp); + + // Brings up libnfc_t4t.a (`nfc_t4t_setup` + `nfc_t4t_emulation_start`). + // The returned handle is zero-sized — all NFC state lives inside the + // library's own statics — so we drop it on return. + NfctDevice::new( + dp.NFCT, + [uuid[0], uuid[1], uuid[2], uuid[3], uuid[4], uuid[5]], + ); + + let mut ctaphid: CtapHidClass = CtapHid::new(usb_bus, ctaphid_rq, 0) + .implements_ctap1() + .implements_ctap2() + .implements_wink(); + ctaphid.set_version(usbd_ctaphid::Version { + major: 0, + minor: 1, + build: 0, + }); + + // Register the CCID USB class: the host enumerates a smartcard reader + // interface and contact APDUs (PIV/opcard/secrets) flow through + // apdu_dispatch alongside the contactless NFC path. + #[cfg(feature = "ccid")] + let ccid: CcidClass = Ccid::new(usb_bus, usb_apdu_rq, Some(b"solo2-nrf")); + // CCID off: don't enumerate the contact interface. The contact APDU + // requester is dropped (apdu_dispatch keeps the responder half so the + // contactless NFC path still works). + #[cfg(not(feature = "ccid"))] + let ccid: CcidClass = { + let _ = usb_apdu_rq; + }; + + let ctaphid_dispatch = CtaphidDispatchDefault::new(ctaphid_rp); + + let usbd = UsbDeviceBuilder::new(usb_bus, UsbVidPid(0x1209, 0xbeee)) + .manufacturer("SoloKeys (port)") + .product("solo2-nrf52840dk") + .serial_number(serial_number) + .device_release(0x0001) + .max_packet_size_0(64) + .composite_with_iads() + .build(); + + let (leds, buttons) = crate::board::init(dp.P0); + + // Storage: + // - `ifs` and `efs` are aliased to the same NVMC-backed filesystem + // (256 KiB at 0x000C_0000). PIV / opcard hardcode some backup paths + // to `Location::External`; pointing efs at the same flash FS makes + // those persist alongside the apps' main state. + // - `vfs` stays RAM-only (8 KiB) for true scratch. + static INTERNAL_STORAGE: StaticCell = StaticCell::new(); + static INTERNAL_FS_ALLOC: StaticCell> = StaticCell::new(); + static VOLATILE_STORAGE: StaticCell = StaticCell::new(); + static VOLATILE_FS_ALLOC: StaticCell> = StaticCell::new(); + + let internal_storage = + INTERNAL_STORAGE.init(InternalFlashStorage::new(dp.NVMC)) as *mut InternalFlashStorage; + let internal_alloc = + INTERNAL_FS_ALLOC.init(Filesystem::allocate()) as *mut Allocation; + let volatile_storage = VOLATILE_STORAGE.init(VolatileStorage::new()) as *mut VolatileStorage; + let volatile_alloc = + VOLATILE_FS_ALLOC.init(Filesystem::allocate()) as *mut Allocation; + + // Try to mount internal FS without formatting; format both on failure. + let needs_format = Filesystem::mount(unsafe { &mut *internal_alloc }, unsafe { + &mut *internal_storage + }) + .is_err(); + + if needs_format { + // Wipes every persistent app state (oath secrets, FIDO credentials, + // …). Surfaces in RTT so a silent reformat doesn't go unnoticed. + defmt::warn!("littlefs2 IFS mount FAILED — reformatting IFS + VFS"); + Filesystem::format(unsafe { &mut *internal_storage }).unwrap(); + Filesystem::format(unsafe { &mut *volatile_storage }).unwrap(); + } + + static INTERNAL_FS: StaticCell> = StaticCell::new(); + let internal_fs = INTERNAL_FS.init( + Filesystem::mount(unsafe { &mut *internal_alloc }, unsafe { + &mut *internal_storage + }) + .unwrap(), + ); + + static VOLATILE_FS: StaticCell> = StaticCell::new(); + let volatile_fs = VOLATILE_FS.init({ + match Filesystem::mount(unsafe { &mut *volatile_alloc }, unsafe { + &mut *volatile_storage + }) { + Ok(fs) => fs, + Err(_) => { + Filesystem::format(unsafe { &mut *volatile_storage }).unwrap(); + Filesystem::mount(unsafe { &mut *volatile_alloc }, unsafe { + &mut *volatile_storage + }) + .unwrap() + } + } + }); + + // efs aliased to the same NVMC filesystem as ifs. + let store = RunnerStore::new(internal_fs, internal_fs, volatile_fs); + + // Run migrations on persistent state before any app touches the + // filesystem. Idempotent: safe on every boot, no-op on already-migrated + // state, no-op on a fresh device whose `fido/dat` directory does not yet + // exist. Mirrors the lpc55 wiring. + { + use trussed::store::Store as _; + let _ = fido_authenticator::state::migrate::migrate_no_rp_dir( + store.ifs(), + littlefs2::path!("fido/dat"), + ); + } + + // Test-only FIDO2 attestation provisioning. Real firmware ships a + // per-device attestation keypair installed at factory; this shortcut + // bakes the public Nitrokey FIDO test PKI (same one `solo-pc` uses for + // its sim runner) into the binary and writes it to LFS at boot whenever + // it's missing. Without this, CTAP1 `Register` and CTAP2 `MakeCredential` + // return `KeyReferenceNotFound (0x6A88)`. Gated by `test-up-control` so + // production builds never include the test key. + #[cfg(feature = "test-up-control")] + { + use trussed::store::Store as _; + const ATTESTATION_CERT: &[u8] = include_bytes!("../../pc/data/fido-cert.der"); + const ATTESTATION_KEY: &[u8] = include_bytes!("../../pc/data/fido-key.trussed"); + let ifs = store.ifs(); + let _ = ifs.create_dir_all(littlefs2::path!("fido/x5c")); + let _ = ifs.create_dir_all(littlefs2::path!("fido/sec")); + let rc = ifs.write(littlefs2::path!("fido/x5c/00"), ATTESTATION_CERT); + let rk = ifs.write(littlefs2::path!("fido/sec/00"), ATTESTATION_KEY); + defmt::warn!( + "test-up-control: prov FIDO attestation cert.write={=bool} key.write={=bool}", + rc.is_ok(), + rk.is_ok(), + ); + } + + let dev_rng = Rng::new(dp.RNG); + let board = Board::new( + dev_rng, + store, + crate::board::UserInterface::new(leds, buttons), + ); + let service = trussed::service::Service::with_dispatch(board, Dispatch::default()); + let mut trussed = Trussed::new(service); + + let version: u32 = 0; + let apps = Apps::new(&mut trussed, uuid, version); + + BoardComponents { + trussed, + apps, + ctaphid_dispatch, + apdu_dispatch, + nfc_apdu_rq, + usbd, + ctaphid, + ccid, + power: dp.POWER, + } +} diff --git a/runners/nrf52840dk/src/main.rs b/runners/nrf52840dk/src/main.rs new file mode 100644 index 0000000..bea6f6a --- /dev/null +++ b/runners/nrf52840dk/src/main.rs @@ -0,0 +1,341 @@ +//! Solo2 firmware port for the Nordic nRF52840-DK. +//! +//! All transports run together: CTAPHID + CCID over USB, plus NFC. Every +//! app (admin/fido/ndef/secrets/piv/opcard) is reachable over both the +//! contact (USB-CCID) and contactless (NFC) interfaces — validated on the +//! DK with CTAP2.3 over NFC while PIV runs over USB-CCID simultaneously. + +#![no_std] +#![no_main] + +use defmt_rtt as _; +use panic_halt as _; + +mod board; +mod cap_touch; +mod dispatch; +mod flash; +mod initializer; +mod nfct; +mod types; + +// HardFault diagnostic handler. Logs the stacked exception frame + +// fault-status registers via defmt RTT, then halts. Replaces the +// silent `loop {}` cortex-m-rt installs by default, which leaves the +// chip dead with no clue why. flip-link's stack-bottom-fence triggers +// a MemManage fault on stack overflow, which escalates to HardFault — +// so this is also our stack-overflow detector. +#[cortex_m_rt::exception] +unsafe fn HardFault(ef: &cortex_m_rt::ExceptionFrame) -> ! { + let scb = unsafe { &*cortex_m::peripheral::SCB::PTR }; + let hfsr = scb.hfsr.read(); + let cfsr = scb.cfsr.read(); + let mmfar = scb.mmfar.read(); + let bfar = scb.bfar.read(); + defmt::error!( + "HardFault! pc={=u32:08x} lr={=u32:08x} r0={=u32:08x} r1={=u32:08x} \ + r2={=u32:08x} r3={=u32:08x} r12={=u32:08x} psr={=u32:08x} | \ + HFSR={=u32:08x} CFSR={=u32:08x} MMFAR={=u32:08x} BFAR={=u32:08x}", + ef.pc(), + ef.lr(), + ef.r0(), + ef.r1(), + ef.r2(), + ef.r3(), + ef.r12(), + ef.xpsr(), + hfsr, + cfsr, + mmfar, + bfar, + ); + // wfi(), not bkpt(). Without a debugger attached, BKPT #0 raises + // another exception inside the HardFault handler, which the Cortex-M + // escalates to a CPU LOCKUP → silent chip reset (RESETREAS bit 3) → + // we lose the fault context entirely. wfi() is safe to spin on. + loop { + cortex_m::asm::wfi(); + } +} + +use ctaphid_dispatch::DEFAULT_MESSAGE_SIZE as CTAPHID_MESSAGE_SIZE; + +// nRF52840 high-frequency clock is 64 MHz; SysTick runs from it. +const SYSTICK_FREQ_HZ: u32 = 64_000_000; + +#[rtic::app(device = nrf52840_pac, peripherals = true, dispatchers = [SWI3_EGU3, SWI4_EGU4])] +mod app { + use super::SYSTICK_FREQ_HZ; + use rtic_monotonics::systick::prelude::*; + systick_monotonic!(Mono, 1000); + use crate::initializer::{init_board, CcidClass, CtapHidClass, UsbBus}; + use crate::nfct; + use crate::types::{Apps, Trussed}; + use apdu_dispatch::dispatch::ApduDispatch; + use apdu_dispatch::interchanges as apdu_interchanges; + use ctaphid_dispatch::DefaultDispatch as CtaphidDispatchDefault; + use embedded_time::duration::Milliseconds; + use nrf52840_pac::POWER; + use rtic_sync::channel::{Receiver, Sender}; + use rtic_sync::make_channel; + use usb_device::device::UsbDevice; + + #[shared] + struct Shared { + trussed: Trussed, + apps: Apps, + ctaphid_dispatch: CtaphidDispatchDefault<'static, 'static>, + apdu_dispatch: ApduDispatch<'static>, + // NFC APDU interchange — t4t bridge in nfct.rs pushes inbound APDUs + // from the reader here and pulls responses to send back. + nfc_apdu_rq: apdu_interchanges::Requester<'static>, + usbd: UsbDevice<'static, UsbBus>, + ctaphid: CtapHidClass, + ccid: CcidClass, + // CTAPHID KEEPALIVE channel — mirrors the LPC55 runner's + // pattern. Idle + on_usb call `ctaphid.did_start_processing()` + // when a new CBOR command arrives; that returns + // `Status::ReceivedData(ms)` indicating when the first + // keepalive frame is due. Send that to the keepalive task, + // which sleeps `ms` then transmits the keepalive. Without + // these frames the chip is silent during long crypto + // operations (e.g. allow_list iteration with 3× chacha8 + // decrypt) and CTAPHID hosts time out — most visibly inside + // UTM, where the Linux hidapi timeout is tighter than + // macOS's. + ctaphid_keepalive_sender: Sender<'static, Milliseconds, 1>, + } + + #[local] + struct Local { + power: POWER, + ctaphid_keepalive_receiver: Receiver<'static, Milliseconds, 1>, + } + + #[init] + fn init(ctx: init::Context) -> (Shared, Local) { + // SysTick monotonic — 1 kHz, used by UserInterface::uptime. (DWT + // would wrap at ~67 s and panic trussed's user-presence loop.) + Mono::start(ctx.core.SYST, SYSTICK_FREQ_HZ); + + let board = init_board(ctx.device); + + let (ctaphid_keepalive_sender, ctaphid_keepalive_receiver) = make_channel!(Milliseconds, 1); + ctaphid_keepalive::spawn().unwrap(); + ndef_clock::spawn().unwrap(); + + ( + Shared { + trussed: board.trussed, + apps: board.apps, + ctaphid_dispatch: board.ctaphid_dispatch, + apdu_dispatch: board.apdu_dispatch, + nfc_apdu_rq: board.nfc_apdu_rq, + usbd: board.usbd, + ctaphid: board.ctaphid, + ccid: board.ccid, + ctaphid_keepalive_sender, + }, + Local { + power: board.power, + ctaphid_keepalive_receiver, + }, + ) + } + + /// Idle: drain NFC + APDU + CTAPHID + USB. The contactless drain also + /// runs in a separate `nfc_drain` task so reader reads don't have to + /// wait for the idle loop to come around. + #[idle(shared = [apps, ctaphid_dispatch, apdu_dispatch, nfc_apdu_rq, usbd, ctaphid, #[cfg(feature = "ccid")] ccid, ctaphid_keepalive_sender])] + fn idle(mut ctx: idle::Context) -> ! { + loop { + // Run the contactless drain inline once per loop too, in + // case nfc_drain raced and an APDU is sitting in the + // mailbox without an IRQ pending it. + ctx.shared.nfc_apdu_rq.lock(nfct::fido_poll); + + let _ = ctx.shared.apps.lock(|apps| { + ctx.shared + .apdu_dispatch + .lock(|disp| apps.apdu_dispatch(|app_slice| disp.poll(app_slice))) + }); + + #[cfg(feature = "ccid")] + ctx.shared.ccid.lock(|ccid| ccid.check_for_app_response()); + + let pending = ctx.shared.apps.lock(|apps| { + ctx.shared + .ctaphid_dispatch + .lock(|disp| apps.ctaphid_dispatch(|app_slice| disp.poll(app_slice))) + }); + if pending { + rtic::pend(nrf52840_pac::Interrupt::USBD); + } + + let ka_status = ctx.shared.usbd.lock(|usbd| { + ctx.shared.ctaphid.lock(|ctaphid| { + ctaphid.check_for_app_response(); + #[cfg(feature = "ccid")] + let _ = ctx.shared.ccid.lock(|ccid| usbd.poll(&mut [ctaphid, ccid])); + #[cfg(not(feature = "ccid"))] + let _ = usbd.poll(&mut [ctaphid]); + ctaphid.did_start_processing() + }) + }); + if let usbd_ctaphid::types::Status::ReceivedData(ms) = ka_status { + ctx.shared + .ctaphid_keepalive_sender + .lock(|s| s.try_send(ms).ok()); + } + } + } + + #[task(binds = USBD, priority = 6, shared = [usbd, ctaphid, #[cfg(feature = "ccid")] ccid, ctaphid_keepalive_sender])] + fn on_usb(mut ctx: on_usb::Context) { + let ka_status = ctx.shared.usbd.lock(|usbd| { + ctx.shared.ctaphid.lock(|ctaphid| { + #[cfg(feature = "ccid")] + let _ = ctx.shared.ccid.lock(|ccid| usbd.poll(&mut [ctaphid, ccid])); + #[cfg(not(feature = "ccid"))] + let _ = usbd.poll(&mut [ctaphid]); + ctaphid.did_start_processing() + }) + }); + if let usbd_ctaphid::types::Status::ReceivedData(ms) = ka_status { + ctx.shared + .ctaphid_keepalive_sender + .lock(|s| s.try_send(ms).ok()); + } + } + + /// CTAPHID KEEPALIVE pump. Receives a deadline from `did_start_processing` + /// (when a new CBOR command arrives), waits that long, then transmits + /// a `KEEPALIVE` frame to the host so it doesn't time out mid-operation. + /// If `send_keepalive` itself returns `ReceivedData(ms)` (i.e. another + /// command landed during transmit) we re-arm via the channel. + /// + /// Mirrors the LPC55 runner's `ctaphid_keepalive` task. + #[task(shared = [ctaphid, ctaphid_keepalive_sender], local = [ctaphid_keepalive_receiver], priority = 6)] + async fn ctaphid_keepalive(mut ctx: ctaphid_keepalive::Context) { + loop { + let ms = ctx.local.ctaphid_keepalive_receiver.recv().await.unwrap(); + Mono::delay(ms.0.millis()).await; + let next = ctx + .shared + .ctaphid + .lock(|ctaphid| ctaphid.send_keepalive(false)); + if let usbd_ctaphid::types::Status::ReceivedData(ms) = next { + let _ = ctx.local.ctaphid_keepalive_receiver.try_recv(); + ctx.shared + .ctaphid_keepalive_sender + .lock(|s| s.try_send(ms).ok()); + } + } + } + + /// Free-running ms clock for NDEF suppression. Advances a counter so the + /// `NdefFidoGate` wrapper can refuse NDEF SELECT for `NDEF_SUPPRESS_MS` + /// after any FIDO command — stops phones popping the URL during a WebAuthn + /// ceremony (the OS selects the FIDO applet first, which arms the window). + #[task(priority = 1)] + async fn ndef_clock(_: ndef_clock::Context) { + const REFRESH_MS: u32 = 50; + loop { + Mono::delay(REFRESH_MS.millis()).await; + crate::types::NDEF_CLOCK_MS + .fetch_add(REFRESH_MS, core::sync::atomic::Ordering::Relaxed); + } + } + + /// Contactless dispatch — pended from the NFCT IRQ whenever an + /// inbound APDU lands in the t4t bridge mailbox. Sits at priority + /// 2, above the trussed syscall ISR (priority 1). The syscall ISR + /// busy-loops inside `confirm_user_present`; if nfc_drain were + /// below it, NDEF reads would never get serviced during a long + /// user-presence wait. Below USBD (3) and NFCT (4) so USB polling + /// and chip IRQs still preempt promptly. + #[task(binds = SWI1_EGU1, priority = 2, shared = [apps, apdu_dispatch, nfc_apdu_rq])] + fn nfc_drain(mut ctx: nfc_drain::Context) { + defmt::info!("nfc_drain: enter"); + // 1. Push any newly-arrived APDU from INCOMING into the + // contactless interchange. + ctx.shared.nfc_apdu_rq.lock(nfct::fido_poll); + // 2. Run apdu_dispatch so ndef-app (or whoever owns the AID) + // builds a response. + let r = ctx.shared.apps.lock(|apps| { + ctx.shared + .apdu_dispatch + .lock(|disp| apps.apdu_dispatch(|app_slice| disp.poll(app_slice))) + }); + defmt::info!("nfc_drain: dispatch={:?}", defmt::Debug2Format(&r)); + // 3. Drain the response back through t4t to the reader. + ctx.shared.nfc_apdu_rq.lock(nfct::fido_poll); + } + + /// Trussed syscall — pended by `Syscall::syscall()` via + /// `rtic::pend(SWI0_EGU0)`. Must run at a priority HIGHER than + /// any task that locks resources `idle` is holding when an app + /// inside ctaphid_dispatch / apdu_dispatch hits + /// `syscall!(trussed.…)`. The shared resource ceiling for those + /// (`apps`, `apdu_dispatch`, `ctaphid_dispatch`) is `nfc_drain`'s + /// priority (= 2): locking them sets BASEPRI to NVIC-prio-6, + /// which masks any task ≤ priority 2. A syscall task at priority + /// 1 was therefore never able to preempt — apps busy-polled in + /// `::poll`, the service + /// never ran, every CTAPHID_MSG / CTAPHID_CBOR timed out. + /// + /// Priority 3 sits above `nfc_drain` (2) and at the same level + /// as `USBD` (3) — they share no resources, so RTIC schedules + /// them cooperatively without preemption hazards. Matches the + /// pattern the lpc55 runner uses (its Trussed syscall is at + /// priority 5). + #[task(binds = SWI0_EGU0, priority = 3, shared = [trussed])] + fn syscall(mut ctx: syscall::Context) { + use rtic_monotonics::Monotonic; + let t0 = Mono::now(); + ctx.shared.trussed.lock(|t| t.process()); + let dt = (Mono::now() - t0).to_millis(); + if dt > 30 { + defmt::warn!("syscall: process took {=u32}ms", dt); + } + } + + #[task(binds = POWER_CLOCK, priority = 5, local = [power])] + fn on_power(ctx: on_power::Context) { + let p = ctx.local.power; + if p.events_usbdetected.read().bits() != 0 { + p.events_usbdetected.write(|w| unsafe { w.bits(0) }); + } + if p.events_usbpwrrdy.read().bits() != 0 { + p.events_usbpwrrdy.write(|w| unsafe { w.bits(0) }); + } + if p.events_usbremoved.read().bits() != 0 { + p.events_usbremoved.write(|w| unsafe { w.bits(0) }); + } + } + + // NFCT + TIMER4 IRQ trampolines into libnrfx_nfct.a. TIMER instance + // 4 is hardwired by NRFX_NFCT_CONFIG_TIMER_INSTANCE_ID in the .a's + // build config (see components/nrf-nfc/Makefile). + // + // Priority 7 (highest): NFCT frame RX/TX is hard real-time (ISO-14443 + // FDT is ~86 us). It MUST sit above USBD (6) and POWER_CLOCK (5) — if + // the USB ISR preempts NFCT mid-frame under CCID/CTAPHID load, the + // anti-collision/RATS reply is corrupted or late and the reader + // abandons activation (symptom: NFCT IRQ storms, t4t never latches + // FIELD_ON, PC/SC reader sees no card). + #[task(binds = NFCT, priority = 7)] + fn nfct_irq(_: nfct_irq::Context) { + unsafe { nrf_nfc::nrfx_nfct::nrfx_nfct_irq_handler() }; + // Wake the contactless drain. The .a's IRQ handler may have + // pushed an APDU into INCOMING via the t4t callback; pending + // nfc_drain here lets it run even when idle is parked in a + // user-presence spin. + rtic::pend(nrf52840_pac::Interrupt::SWI1_EGU1); + } + + #[task(binds = TIMER4, priority = 7)] + fn nfct_timer(_: nfct_timer::Context) { + unsafe { nrf_nfc::nrfx_nfct::nrfx_nfct_workaround_timer_handler() }; + } +} diff --git a/runners/nrf52840dk/src/nfct.rs b/runners/nrf52840dk/src/nfct.rs new file mode 100644 index 0000000..22f3f96 --- /dev/null +++ b/runners/nrf52840dk/src/nfct.rs @@ -0,0 +1,286 @@ +//! nRF52840 NFCT Type 4A PICC, backed by Nordic's `libnfc_t4t.a`. +//! +//! The library runs in raw ISO-DEP mode: anti-collision, RATS/ATS, I-block +//! framing, chaining and WTX are handled inside the blob. APDUs arrive at +//! `t4t_callback` as `DATA_IND` events (possibly fragmented), are +//! reassembled into full C-APDUs, and pushed onto the apdu-dispatch +//! interchange — `ndef-app`, `fido-authenticator`, et al. handle them in +//! the idle loop. +//! +//! IRQ context (NFCT, priority 4) cannot call into the trussed stack +//! directly (trussed crypto runs at priority 2 and would deadlock), so the +//! IRQ stashes inbound APDUs into a single-slot mailbox and the idle loop +//! drains them via `fido_poll`. + +use apdu_dispatch::interchanges; +use core::sync::atomic::{AtomicBool, Ordering}; +use defmt::info; +use heapless::Vec; +use nrf_nfc::nfc_t4t as t4t; + +// SAFETY (DATA_IND callback): runs in NFCT IRQ context. We touch only +// `cortex_m::interrupt::Mutex>`-protected statics and the +// `FIELD_ON` atomic. The `slice::from_raw_parts(data, data_length)` is sound +// only while we're inside this callback — t4t_lib owns the buffer and it +// stays valid until we return. +extern "C" fn t4t_callback( + _ctx: *mut core::ffi::c_void, + event: t4t::nfc_t4t_event_t, + data: *const u8, + data_length: usize, + flags: u32, +) { + info!( + "t4t cb evt={=u32} len={=usize} flags={=u32:#x}", + event, data_length, flags + ); + match event { + t4t::nfc_t4t_event_t_NFC_T4T_EVENT_FIELD_ON => { + FIELD_ON.store(true, Ordering::Release); + // Per-session reset: a partial APDU left over from a previous tap + // (reader disconnects mid-ceremony) would otherwise prepend + // garbage to the next session's first command. + cortex_m::interrupt::free(|cs| { + APDU_ACC.borrow(cs).borrow_mut().clear(); + *INCOMING.borrow(cs).borrow_mut() = None; + }); + HAS_INCOMING.store(false, Ordering::Release); + } + t4t::nfc_t4t_event_t_NFC_T4T_EVENT_FIELD_OFF => { + FIELD_ON.store(false, Ordering::Release); + } + t4t::nfc_t4t_event_t_NFC_T4T_EVENT_DATA_IND => { + // Long C-APDUs arrive in fragments; concatenate until MORE=0. + let more = (flags & t4t::nfc_t4t_data_ind_flags_t_NFC_T4T_DI_FLAG_MORE) != 0; + let slice = if !data.is_null() && data_length > 0 { + unsafe { core::slice::from_raw_parts(data, data_length) } + } else { + &[] + }; + cortex_m::interrupt::free(|cs| { + let mut acc = APDU_ACC.borrow(cs).borrow_mut(); + let _ = acc.extend_from_slice(slice); + if !more { + let mut v: Vec = Vec::new(); + let _ = v.extend_from_slice(&acc); + acc.clear(); + *INCOMING.borrow(cs).borrow_mut() = Some(v); + HAS_INCOMING.store(true, Ordering::Release); + } + }); + } + _ => {} + } +} + +// FIDO MakeCredential responses (CBOR + attestation cert + signature) hit +// ~1.5–2 KiB; size for headroom. +const TX_RESP_SIZE: usize = 4096; + +// SAFETY (TX_RESP): writes are serialized below by `interrupt::free`. The +// t4t library reads asynchronously after `nfc_t4t_response_pdu_send` returns, +// but the reader side is synchronous (one APDU outstanding at a time), so +// back-to-back `send_response` calls don't race a still-in-flight prior TX. +static mut TX_RESP: [u8; TX_RESP_SIZE] = [0; TX_RESP_SIZE]; + +fn send_response(buf: &[u8]) { + // Critical section: send_response is called from the idle loop, but the + // NFCT IRQ may also touch shared state mid-call. Serializing keeps + // TX_RESP consistent if a future code path ever re-enters. + cortex_m::interrupt::free(|_cs| unsafe { + let n = buf.len().min(TX_RESP_SIZE); + let dst = core::ptr::addr_of_mut!(TX_RESP) as *mut u8; + core::ptr::copy_nonoverlapping(buf.as_ptr(), dst, n); + let _ = t4t::nfc_t4t_response_pdu_send(dst, n); + }); +} + +// ── IRQ → idle bridge ──────────────────────────────────────────────────────── + +const MAX_APDU: usize = interchanges::SIZE; + +// Single-slot mailbox: IRQ pushes one assembled APDU; idle drains it. +static INCOMING: cortex_m::interrupt::Mutex>>> = + cortex_m::interrupt::Mutex::new(core::cell::RefCell::new(None)); + +// Lock-free mirror of `INCOMING.is_some()`. `fido_poll` runs in a tight idle +// loop; reading this atomic on the common no-data path avoids a +// `cortex_m::interrupt::free` PRIMASK section, which would mask the priority-7 +// NFCT IRQ and corrupt in-flight ISO-DEP frames (anti-collision/RATS fails). +static HAS_INCOMING: AtomicBool = AtomicBool::new(false); + +// Reassembly buffer for fragmented DATA_IND chunks. +static APDU_ACC: cortex_m::interrupt::Mutex>> = + cortex_m::interrupt::Mutex::new(core::cell::RefCell::new(Vec::new())); + +/// `true` while a reader's RF field is up. The board's UserInterface impl +/// reads this so user-presence checks auto-approve during NFC sessions +/// (the tap itself is the consent signal — there's no button to press +/// while the phone is on the antenna). +pub static FIELD_ON: AtomicBool = AtomicBool::new(false); + +pub fn field_on() -> bool { + FIELD_ON.load(Ordering::Acquire) +} + +/// Drain incoming APDU into apdu-dispatch and ship pending response back. +/// Must be called from idle every loop iteration. +/// The Nordic t4t lib drops a byte when fragmenting a response PDU that +/// exceeds one ISO-DEP frame (FSC = 256): apdu-dispatch serves the reader's +/// `Le`=256, so a 256-data + SW = 258-byte chunk overruns a single I-block +/// and the closed lib's fragmentation loses a byte at the boundary. Cap the +/// host's requested `Le` so every 61xx chunk (data + SW) fits one I-block; +/// the reader then GET-RESPONSEs for the rest. Short APDUs only (the t4t lib +/// can't parse extended-length requests anyway). lpc55/fm11nc08 is unaffected. +const NFC_MAX_LE: u8 = 250; + +fn cap_le(apdu: &mut [u8]) { + let n = apdu.len(); + let le_idx = if n == 5 { + Some(4) // Case 2: header(4) + Le + } else if n >= 7 && n == 6 + apdu[4] as usize { + Some(n - 1) // Case 4: header(4) + Lc(1) + data + Le(1) + } else { + None // Case 1/3 (no Le) or a chained intermediate frame + }; + if let Some(i) = le_idx { + if apdu[i] == 0 || apdu[i] > NFC_MAX_LE { + apdu[i] = NFC_MAX_LE; + } + } +} + +pub fn fido_poll(rq: &mut interchanges::Requester<'static>) { + // Common no-data path is a single atomic load — no PRIMASK section. Only + // enter `cortex_m::interrupt::free` (which masks the priority-7 NFCT IRQ) + // when there's actually an APDU to take, so the tight idle-loop poll never + // stalls in-flight ISO-DEP frames. + let has_data = HAS_INCOMING.load(Ordering::Acquire); + let pending = if has_data { + cortex_m::interrupt::free(|cs| { + HAS_INCOMING.store(false, Ordering::Release); + INCOMING.borrow(cs).borrow_mut().take() + }) + } else { + None + }; + if let Some(mut apdu) = pending { + cap_le(&mut apdu); + info!( + "fido_poll: pushing apdu len={=usize} bytes={=[u8]:#x}", + apdu.len(), + apdu.as_slice() + ); + if let Ok(data) = interchanges::Data::from_slice(&apdu) { + match rq.request(data) { + Ok(_) => info!("fido_poll: rq.request OK"), + Err(_) => info!("fido_poll: rq.request FAILED (busy)"), + } + } + } + if rq.state() == interchange::State::Responded { + if let Some(resp) = rq.take_response() { + info!("fido_poll: sending response len={=usize}", resp.len()); + send_response(&resp); + } + } +} + +pub struct NfctDevice { + _private: (), +} + +impl NfctDevice { + /// Bring up the t4t library. NFCID1 is derived inside the library (via + /// `nfc_platform_nfcid1_default_bytes_get`); the seed argument is unused. + pub fn new(_nfct: nrf52840_pac::NFCT, _uid_seed: [u8; 6]) -> Self { + // ── Errata 57 — NFCT modulation amplitude tuning ───────────────────── + // Nordic considers this errata "officially nRF52832 only" so neither + // nrfx_nfct nor libnfc_t4t apply it for nRF52840. Empirically it is + // required: without these pokes the SENSRES (ATQA) reply goes out at + // insufficient modulation depth and aggressive readers miss it + // and abandon anti-collision. + // + // Sequence: TASKS_DISABLE → ~150 nops settling time → 4 undocumented + // analog tuning registers. Library will TASKS_SENSE later via + // emulation_start. The pokes survive subsequent activate cycles. + unsafe { + core::ptr::write_volatile(0x4000_5004 as *mut u32, 0x0000_0001); // TASKS_DISABLE + for _ in 0..150 { + cortex_m::asm::nop(); + } + core::ptr::write_volatile(0x4000_5610 as *mut u32, 0x0000_0005); + core::ptr::write_volatile(0x4000_5614 as *mut u32, 0x0000_003F); + core::ptr::write_volatile(0x4000_5618 as *mut u32, 0x0000_0000); + core::ptr::write_volatile(0x4000_5688 as *mut u32, 0x0000_0001); + } + + let rc = unsafe { t4t::nfc_t4t_setup(Some(t4t_callback), core::ptr::null_mut()) }; + if rc != 0 { + info!("nfct: nfc_t4t_setup rc={}", rc); + return Self { _private: () }; + } + + // 4-byte NFCID1 (single cascade). Some readers treat 4-byte UID tags + // as NTAG-class and auto-read NDEF; with 7-byte UIDs they probe for + // FIDO and DESELECT if not satisfied (observed empirically). Passing + // the length value as the parameter payload tells the library to use + // a default-derived NFCID1 of that length. + let mut nfcid_len: u8 = 4; + let _ = unsafe { + t4t::nfc_t4t_parameter_set( + t4t::nfc_t4t_param_id_t_NFC_T4T_PARAM_NFCID1, + &mut nfcid_len as *mut u8 as *mut core::ffi::c_void, + 1, + ) + }; + + // Raise the Frame-Waiting-Time ceiling to the NFC max (FWI_MAX = 8) + // so the library's WTX can hold a strict PC/SC reader through slow + // on-card crypto (ES256 keygen/sign; ML-DSA is ~100s of ms). Without + // it the reader times out mid-MakeCredential ("transaction failed"); + // a phone is lenient. Payload is the fwi_max enum byte. + let mut fwi_max: u8 = t4t::nfc_t4t_fwi_max_val_t_NFC_T4T_FWI_MAX_VAL_NFC as u8; + let _ = unsafe { + t4t::nfc_t4t_parameter_set( + t4t::nfc_t4t_param_id_t_NFC_T4T_PARAM_FWI_MAX, + &mut fwi_max as *mut u8 as *mut core::ffi::c_void, + 1, + ) + }; + + // Force the SAK "Protocol" bits to T4AT (ISO 14443-4) before + // starting emulation. `nfc_t4t_setup` doesn't set this and the + // chip's reset value advertises a cascade-tag bit on a 4-byte + // UID, which iPhone treats as a malformed NFC-A tag and stops + // chunked NDEF reads after the first ReadBinary. + { + use nrf_nfc::nrfx_nfct::{ + nrf_nfct_selres_protocol_t_NRF_NFCT_SELRES_PROTOCOL_T4AT, + nrfx_nfct_param_id_t_NRFX_NFCT_PARAM_ID_SEL_RES, nrfx_nfct_param_t, + nrfx_nfct_param_t__bindgen_ty_1, nrfx_nfct_parameter_set, + }; + let p = nrfx_nfct_param_t { + id: nrfx_nfct_param_id_t_NRFX_NFCT_PARAM_ID_SEL_RES, + data: nrfx_nfct_param_t__bindgen_ty_1 { + sel_res_protocol: nrf_nfct_selres_protocol_t_NRF_NFCT_SELRES_PROTOCOL_T4AT + as u8, + }, + }; + let rc = unsafe { nrfx_nfct_parameter_set(&p) }; + if rc != 0 { + info!("nfct: SEL_RES set rc={}", rc); + } + } + + // Raw ISO-DEP mode — DATA_IND callbacks deliver APDU fragments. + // (Skipping ndef_*_payload_set selects raw mode in the library.) + + let rc = unsafe { t4t::nfc_t4t_emulation_start() }; + if rc != 0 { + info!("nfct: emulation_start rc={}", rc); + } + + Self { _private: () } + } +} diff --git a/runners/nrf52840dk/src/types.rs b/runners/nrf52840dk/src/types.rs new file mode 100644 index 0000000..ee4b4ac --- /dev/null +++ b/runners/nrf52840dk/src/types.rs @@ -0,0 +1,348 @@ +//! Trussed platform + apps wiring for the nRF52840-DK runner. +//! +//! Default app set: admin, fido, ndef, secrets, piv, opcard. + +/// Pack `CARGO_PKG_VERSION_*` into the same u32 layout the LPC55 runner uses +/// for `firmware_version` (CTAP 2.1 §6.4 0x0E): `(major << 22) | (minor << 6) +/// | patch`. Major < 1024, minor < 16384, patch < 64; built from the package +/// version at compile time. +const fn pkg_version_u32() -> u32 { + const fn parse(s: &str) -> u32 { + let bytes = s.as_bytes(); + let mut acc = 0u32; + let mut i = 0; + while i < bytes.len() { + acc = acc * 10 + (bytes[i] - b'0') as u32; + i += 1; + } + acc + } + let major = parse(env!("CARGO_PKG_VERSION_MAJOR")); + let minor = parse(env!("CARGO_PKG_VERSION_MINOR")); + let patch = parse(env!("CARGO_PKG_VERSION_PATCH")); + (major << 22) | (minor << 6) | patch +} + +use cortex_m::peripheral::SCB; +use generic_array::typenum::{U128, U8}; +use littlefs2::const_ram_storage; +use nrf52840_hal::rng::Rng; +use trussed::client::MultiplexedClient; +use trussed::interrupt::InterruptFlag; +use trussed::platform; +use trussed::store::DynFilesystem; + +use crate::board::{Syscall, UserInterface}; +use crate::dispatch::Dispatch; +use solo_apps::client::{client_tag, make_client}; +use solo_apps::ndef::{FidoNdefStamp, NdefFidoGate}; + +// ───── Reboot impl required by admin-app ───────────────────────────────────── + +pub struct Reboot; +impl admin_app::Reboot for Reboot { + fn reboot() -> ! { + SCB::sys_reset() + } + /// Reboot into the Nordic Open Bootloader's DFU mode. + /// `GPREGRET = 0xB1` is the SDK-defined BOOTLOADER_DFU_START sentinel — + /// the bootloader checks this byte first thing on reset and enters DFU + /// (USB CDC) mode if it sees it. The register is in retained RAM, so it + /// survives the soft reset. + fn reboot_to_firmware_update() -> ! { + const BOOTLOADER_DFU_START: u8 = 0xB1; + unsafe { + (*nrf52840_pac::POWER::PTR) + .gpregret + .write(|w| w.gpregret().bits(BOOTLOADER_DFU_START)); + } + SCB::sys_reset() + } + fn reboot_to_firmware_update_destructive() -> ! { + Self::reboot_to_firmware_update() + } + fn locked() -> bool { + false + } +} + +// ───── Storage ─────────────────────────────────────────────────────────────── + +const_ram_storage!( + name = VolatileStorage, + erase_value = 0xff, + read_size = 1, + write_size = 1, + cache_size_ty = U128, + block_size = 128, + block_count = 8192 / 104, + lookahead_size_ty = U8, + filename_max_plus_one_ty = generic_array::typenum::U256, + path_max_plus_one_ty = generic_array::typenum::U256, +); + +// External storage is aliased to the same NVMC-backed filesystem as +// `ifs` in main.rs — see the RunnerStore construction. piv-authenticator +// and opcard hardcode `Location::External` for a couple of small backup +// paths (PUK / admin-key); routing them at the same flash filesystem +// makes them persist without a separate backing. + +#[derive(Clone, Copy)] +pub struct RunnerStore { + ifs: &'static dyn DynFilesystem, + efs: &'static dyn DynFilesystem, + vfs: &'static dyn DynFilesystem, +} + +impl RunnerStore { + pub fn new( + ifs: &'static dyn DynFilesystem, + efs: &'static dyn DynFilesystem, + vfs: &'static dyn DynFilesystem, + ) -> Self { + Self { ifs, efs, vfs } + } +} + +impl trussed::store::Store for RunnerStore { + fn ifs(&self) -> &dyn DynFilesystem { + self.ifs + } + fn efs(&self) -> &dyn DynFilesystem { + self.efs + } + fn vfs(&self) -> &dyn DynFilesystem { + self.vfs + } +} + +pub type Store = RunnerStore; + +// ───── Platform ────────────────────────────────────────────────────────────── + +platform!( + Board, + R: Rng, + S: Store, + UI: UserInterface, +); + +// ───── Trussed Service wrapper + Backend slates ────────────────────────────── + +/// All apps share the channel inside the multiplexed service and are +/// distinguished by a per-app `ClientTag`. +pub type TrussedClient = MultiplexedClient; + +/// The multiplexed service wrapper (`Trussed`), backend slates, `client_tag`, +/// and `make_client` are shared with the lpc55 runner via `solo_apps::client`. +pub type Trussed = solo_apps::client::Trussed; + +// ───── App types ───────────────────────────────────────────────────────────── + +#[derive(Default)] +pub struct AdminStatus { + random_error: bool, +} + +impl admin_app::StatusBytes for AdminStatus { + type Serialized = [u8; 1]; + fn set_random_error(&mut self, value: bool) { + self.random_error = value; + } + fn get_random_error(&self) -> bool { + self.random_error + } + fn serialize(&self) -> Self::Serialized { + [self.random_error as u8] + } +} + +pub type AdminApp = admin_app::App; +pub type FidoApp = fido_authenticator::Authenticator; +pub type NdefApp = ndef_app::App; +pub type SecretsApp = secrets_app::Authenticator; +pub type PivApp = piv_authenticator::Authenticator; +pub type OpcardApp = opcard::Card; + +/// NDEF-suppression clock. The NDEF app refuses `SELECT` (so phones don't pop +/// the tag during/after a FIDO ceremony) while we're within `NDEF_SUPPRESS_MS` +/// of the last FIDO command. `NDEF_CLOCK_MS` is a free-running ms counter ticked +/// by the `ndef_clock` task; `NDEF_LAST_FIDO_MS` is stamped on every FIDO +/// select/call. The clock starts at `NDEF_SUPPRESS_MS` and `NDEF_LAST_FIDO_MS` +/// at 0, so at boot the gap is already `>= NDEF_SUPPRESS_MS` (NOT suppressed) — +/// the tag stays readable until an actual FIDO command arms the window. +pub static NDEF_CLOCK_MS: core::sync::atomic::AtomicU32 = + core::sync::atomic::AtomicU32::new(NDEF_SUPPRESS_MS); +pub static NDEF_LAST_FIDO_MS: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(0); +const NDEF_SUPPRESS_MS: u32 = 3000; + +/// nrf52840dk NDEF timebase for `solo_apps::ndef`: a free-running ms counter +/// ticked by the `ndef_clock` task. `set_fido_over_nfc` stays the default no-op +/// (the contactless-presence hook is not yet wired on the DK). +pub struct NrfNfcClock; +impl solo_apps::ndef::NfcClock for NrfNfcClock { + const SUPPRESS_WINDOW: u32 = NDEF_SUPPRESS_MS; + fn now() -> u32 { + NDEF_CLOCK_MS.load(core::sync::atomic::Ordering::Relaxed) + } + fn last_fido() -> &'static core::sync::atomic::AtomicU32 { + &NDEF_LAST_FIDO_MS + } +} + +use apdu_dispatch::App as ApduApp; +use ctaphid_dispatch::app::App as CtaphidApp; + +static ADMIN_INTERRUPT: InterruptFlag = InterruptFlag::new(); +static FIDO_INTERRUPT: InterruptFlag = InterruptFlag::new(); +static SECRETS_INTERRUPT: InterruptFlag = InterruptFlag::new(); +static PIV_INTERRUPT: InterruptFlag = InterruptFlag::new(); +static OPCARD_INTERRUPT: InterruptFlag = InterruptFlag::new(); +static NDEF_INTERRUPT: InterruptFlag = InterruptFlag::new(); + +/// Fire the user-cancel interrupt on every Trussed app. Trussed checks +/// these between syscalls and aborts any in-flight `confirm_user_present` +/// with `consent::Error::Interrupted`. Whichever app is currently being +/// invoked has its flag in `Working` (set by ctaphid-dispatch's call_app +/// at the host-transport layer), so the CAS in `.interrupt()` succeeds. +pub fn interrupt_all_apps() { + ADMIN_INTERRUPT.interrupt(); + FIDO_INTERRUPT.interrupt(); + SECRETS_INTERRUPT.interrupt(); + PIV_INTERRUPT.interrupt(); + OPCARD_INTERRUPT.interrupt(); + NDEF_INTERRUPT.interrupt(); +} + +pub struct Apps { + pub admin: AdminApp, + pub fido: FidoApp, + pub ndef: NdefApp, + pub secrets: SecretsApp, + pub piv: PivApp, + pub opcard: OpcardApp, +} + +impl Apps { + pub fn new(trussed: &mut Trussed, uuid: [u8; 16], version: u32) -> Self { + let admin_client = make_client( + client_tag::ADMIN, + littlefs2::path!("admin"), + trussed, + Some(&ADMIN_INTERRUPT), + &solo_apps::client::STAGING_BACKENDS, + ); + let admin = AdminApp::with_default_config( + admin_client, + uuid, + version, + env!("CARGO_PKG_VERSION"), + AdminStatus::default(), + &[], + ); + + let fido_client = make_client( + client_tag::FIDO, + littlefs2::path!("fido"), + trussed, + Some(&FIDO_INTERRUPT), + &solo_apps::client::STAGING_BACKENDS, + ); + let fido = fido_authenticator::Authenticator::new( + fido_client, + fido_authenticator::Conforming {}, + // Shared with the lpc55 runner; `max_resident` is 50 on the DK. + solo_apps::config::fido_config(pkg_version_u32(), 50), + ); + + let ndef_client = make_client( + client_tag::NDEF, + littlefs2::path!("ndef"), + trussed, + Some(&NDEF_INTERRUPT), + &solo_apps::client::STAGING_BACKENDS, + ); + let ndef = NdefApp::new(ndef_client); + + let secrets_client = make_client( + client_tag::SECRETS, + littlefs2::path!("secrets"), + trussed, + Some(&SECRETS_INTERRUPT), + &solo_apps::client::AUTH_BACKENDS, + ); + let secrets = SecretsApp::new( + secrets_client, + secrets_app::Options::new( + trussed::types::Location::Internal, + 0, // custom_status_reverse_hotp_success + 1, // custom_status_reverse_hotp_error + [uuid[0], uuid[1], uuid[2], uuid[3]], + 50, // max_resident_credentials_allowed + ), + ); + + let piv_client = make_client( + client_tag::PIV, + littlefs2::path!("piv"), + trussed, + Some(&PIV_INTERRUPT), + &solo_apps::client::PIV_BACKENDS, + ); + let piv = PivApp::new( + piv_client, + piv_authenticator::Options::default().storage(trussed::types::Location::Internal), + ); + + let opcard_client = make_client( + client_tag::OPCARD, + littlefs2::path!("opcard"), + trussed, + Some(&OPCARD_INTERRUPT), + &solo_apps::client::PIV_BACKENDS, + ); + let opcard = { + let mut opts = opcard::Options::default(); + opts.storage = trussed::types::Location::Internal; + OpcardApp::new(opcard_client, opts) + }; + + Self { + admin, + fido, + ndef, + secrets, + piv, + opcard, + } + } + + /// Apps that handle CTAPHID frames (fido + admin + secrets via 0x70..). + #[inline(never)] + pub fn ctaphid_dispatch(&mut self, f: F) -> T + where + F: FnOnce(&mut [&mut dyn CtaphidApp<'static>]) -> T, + { + f(&mut [&mut self.admin, &mut self.fido, &mut self.secrets]) + } + + /// Apps that handle APDUs over CCID/NFC. More specific AIDs first. + /// + /// `secrets` (Yubico OATH AID `A0 00 00 05 27 21 01`) is intentionally + /// omitted from the NFC slate: some phone NFC stacks match it before + /// reaching the FIDO AID, which prevents WebAuthn flows from entering + /// the FIDO dialog. It's still reachable via CTAPHID over USB. + #[inline(never)] + pub fn apdu_dispatch(&mut self, f: F) -> T + where + F: FnOnce(&mut [&mut dyn ApduApp]) -> T, + { + f(&mut [ + &mut NdefFidoGate::::new(&mut self.ndef), + &mut self.piv, + &mut self.opcard, + &mut FidoNdefStamp::::new(&mut self.fido), + &mut self.admin, + ]) + } +}