diff --git a/.github/workflows/teensy41.yml b/.github/workflows/teensy41.yml new file mode 100644 index 0000000..fec2a3c --- /dev/null +++ b/.github/workflows/teensy41.yml @@ -0,0 +1,33 @@ +name: Teensy 4.1 firmware + +on: + push: + paths: + - 'firmware/teensy41/**' + - '.github/workflows/teensy41.yml' + pull_request: + paths: + - 'firmware/teensy41/**' + - '.github/workflows/teensy41.yml' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - name: Install PlatformIO + run: pip install platformio + - name: Run host BLAKE2s tests + run: | + g++ -std=c++17 -Wall -Wextra -Werror \ + -Ifirmware/teensy41/include \ + firmware/teensy41/src/blake2s.cpp \ + firmware/teensy41/test/blake2s_host.cpp \ + -o /tmp/blake2s-host-test + /tmp/blake2s-host-test + - name: Build Teensy 4.1 firmware + working-directory: firmware/teensy41 + run: pio run -e teensy41 diff --git a/docs/superpowers/plans/2026-08-12-teensy41-tailscale-m1-crypto.md b/docs/superpowers/plans/2026-08-12-teensy41-tailscale-m1-crypto.md new file mode 100644 index 0000000..32ecf76 --- /dev/null +++ b/docs/superpowers/plans/2026-08-12-teensy41-tailscale-m1-crypto.md @@ -0,0 +1,55 @@ +# Teensy 4.1 Tailscale M1 Crypto Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Establish the cryptographic primitives required by Tailscale/WireGuard on Teensy 4.1 and verify them with standard test vectors. + +**Architecture:** Use Monocypher 4.0.3 for X25519 and RFC 8439 ChaCha20-Poly1305 because it is small, portable, and embedded-oriented. Implement BLAKE2s separately from RFC 7693 because WireGuard requires BLAKE2s while Monocypher exposes BLAKE2b. Use the Teensy hardware entropy facility exposed by the Entropy library for random bytes. + +**Tech Stack:** C/C++, PlatformIO, Teensy 4.1, Monocypher 4.0.3, RFC 7693 BLAKE2s, Teensy Entropy. + +## Global Constraints + +- No Tailscale auth key or private production key may be committed. +- X25519 outputs must be checked for the all-zero shared secret before use. +- WireGuard uses BLAKE2s, not BLAKE2b. +- ChaCha20-Poly1305 must use the IETF 96-bit nonce form. +- Secret buffers are wiped after use where practical. +- Test vectors must be deterministic and independent of device-generated randomness. + +### Task 1: Crypto dependency and interface + +**Files:** +- Modify: `firmware/teensy41/platformio.ini` +- Modify: `firmware/teensy41/include/ts_crypto.h` +- Create: `firmware/teensy41/src/ts_crypto.cpp` + +Add Monocypher 4.0.3 as an exact Git dependency. Expose `randomBytes`, X25519 key/public/shared-secret operations, BLAKE2s, and IETF ChaCha20-Poly1305 seal/open operations. + +### Task 2: BLAKE2s implementation + +**Files:** +- Create: `firmware/teensy41/include/blake2s.h` +- Create: `firmware/teensy41/src/blake2s.cpp` +- Create: `firmware/teensy41/test/blake2s_host.c` + +Implement BLAKE2s-256 from RFC 7693 with keyed and unkeyed operation. Validate empty input, `abc`, and a keyed test against independent reference output before using it in the embedded wrapper. + +### Task 3: X25519 and AEAD known-answer tests + +**Files:** +- Create: `firmware/teensy41/test/m1_vectors.cpp` +- Create: `firmware/teensy41/test/M1-CRYPTO.md` + +Use RFC 7748 X25519 vectors and RFC 8439 ChaCha20-Poly1305 vectors. Verify X25519 public/shared results, successful AEAD open, and failure after ciphertext/AAD tampering. + +### Task 4: Embedded self-test + +**Files:** +- Create: `firmware/teensy41/examples/m1_crypto/m1_crypto.ino` + +Run the deterministic vectors on the physical Teensy and print PASS/FAIL without printing secrets beyond public test-vector values. + +### Acceptance + +M1 is complete when the host tests pass, the PlatformIO Teensy build passes, and the physical Teensy self-test reports PASS for BLAKE2s, X25519, AEAD encryption/decryption, tamper rejection, entropy generation, and secure wiping calls compile successfully. diff --git a/firmware/teensy41/docs/m1-crypto.md b/firmware/teensy41/docs/m1-crypto.md new file mode 100644 index 0000000..e17ed00 --- /dev/null +++ b/firmware/teensy41/docs/m1-crypto.md @@ -0,0 +1,19 @@ +# M1 — cryptography + +M1 establishes the cryptographic foundation required before implementing the Tailscale control/data planes. + +## Required primitives + +- CSPRNG / hardware-backed entropy source +- X25519 / Curve25519 +- ChaCha20-Poly1305 +- BLAKE2s +- constant-time operations and secure key wiping + +## Test strategy + +Every primitive gets known-answer tests on the host and the same vectors on Teensy 4.1. Shared-secret agreement is tested with two independent key pairs. AEAD tests cover valid, modified-ciphertext, modified-AAD, and nonce-reuse rejection at the protocol layer. + +Do not put production keys or Tailscale auth keys in examples, tests, CI logs, or source control. + +Tailscale nodes use machine and node key pairs; private keys stay on the device while public node keys are distributed by the control plane. Auth keys are only provisioning credentials and are not a replacement for the node's private key. diff --git a/firmware/teensy41/examples/m1_crypto/m1_crypto.ino b/firmware/teensy41/examples/m1_crypto/m1_crypto.ino new file mode 100644 index 0000000..033c905 --- /dev/null +++ b/firmware/teensy41/examples/m1_crypto/m1_crypto.ino @@ -0,0 +1,106 @@ +#include +#include + +#include "ts_crypto.h" + +static bool equalBytes(const uint8_t *a, const uint8_t *b, size_t n) { + return memcmp(a, b, n) == 0; +} + +static void printHex(const char *label, const uint8_t *data, size_t len) { + Serial.print(label); + for (size_t i = 0; i < len; ++i) { + if (data[i] < 16) Serial.print('0'); + Serial.print(data[i], HEX); + } + Serial.println(); +} + +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println(); + Serial.println("=== Tailscale Teensy 4.1 / M1 ==="); + + static const uint8_t privateKey[32] = { + 0x77,0x07,0x6d,0x0a,0x73,0x18,0xa5,0x7d,0x3c,0x16,0xc1,0x72,0x51,0xb2,0x66,0x45, + 0xdf,0x4c,0x2f,0x87,0xeb,0xc0,0x99,0x2a,0xb1,0x77,0xfb,0xa5,0x1d,0xb9,0x2c,0x2a}; + static const uint8_t expectedPublic[32] = { + 0x85,0x20,0xf0,0x09,0x89,0x30,0xa7,0x54,0x74,0x8b,0x7d,0xdc,0xb4,0x3e,0xf7,0x5a, + 0x0d,0xbf,0x3a,0x0d,0x26,0x38,0x1a,0xf4,0xeb,0xa4,0xa9,0x8e,0xaa,0x9b,0x4e,0x6a}; + + uint8_t publicKey[32]; + if (ts::crypto::x25519PublicKey(publicKey, privateKey) && equalBytes(publicKey, expectedPublic, 32)) { + Serial.println("[PASS] X25519 RFC 7748 public-key vector"); + printHex("public=", publicKey, 32); + } else { + Serial.println("[FAIL] X25519 public-key vector"); + } + + const uint8_t abc[] = {'a', 'b', 'c'}; + const uint8_t expectedBlake[32] = { + 0x50,0x8c,0x5e,0x8c,0x32,0x7c,0x14,0xe2,0xe1,0xa7,0x2b,0xa3,0x4e,0xeb,0x45,0x2f, + 0x37,0x45,0x8b,0x20,0x9e,0xd6,0x3a,0x29,0x4d,0x99,0x9b,0x4c,0x86,0x67,0x59,0x82}; + uint8_t digest[32]; + if (ts::crypto::blake2s(digest, abc, sizeof(abc)) && equalBytes(digest, expectedBlake, 32)) { + Serial.println("[PASS] BLAKE2s-256 abc vector"); + } else { + Serial.println("[FAIL] BLAKE2s-256 abc vector"); + } + + static const uint8_t key[32] = { + 0x80,0x81,0x82,0x83,0x84,0x85,0x86,0x87,0x88,0x89,0x8a,0x8b,0x8c,0x8d,0x8e,0x8f, + 0x90,0x91,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0x9b,0x9c,0x9d,0x9e,0x9f}; + static const uint8_t nonce[12] = {0x40,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x07,0,0,0}; + static const uint8_t aad[12] = {0x50,0x51,0x52,0x53,0xc0,0xc1,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7}; + static const uint8_t plaintext[] = "Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it."; + static const uint8_t expectedCiphertext[114] = { + 0x9a,0xbc,0x18,0xdb,0x71,0x32,0xd3,0x04,0xf6,0x37,0xd6,0x4e,0x40,0x45,0x8a,0x92, + 0x07,0x8d,0xd4,0x9a,0x1d,0x4f,0xbc,0xcb,0x78,0x10,0xd9,0x60,0x1e,0xb3,0xdc,0xd5,0xd3, + 0xa8,0x9a,0x67,0x95,0xaa,0x8b,0x76,0xcc,0x00,0x7c,0x0e,0x24,0x5b,0x0c,0x18,0x72, + 0xd1,0xa5,0x00,0x3c,0x0e,0xb2,0x36,0x4a,0xfa,0x99,0xed,0xc4,0x51,0xb7,0xa6,0xfb, + 0xec,0x73,0x62,0x36,0xf0,0xa9,0x2e,0xbb,0x8a,0xb3,0x5e,0x20,0x81,0x89,0x4c,0xea, + 0x3b,0xc0,0x6c,0x33,0x97,0xb9,0x79,0xdb,0xcd,0x44,0x5f,0x45,0xb3,0x7c,0x4c,0xad, + 0x2b,0x60,0xe1,0x80,0xa6,0x42,0xfd,0xe7,0x20,0x37,0x48,0x03,0x4c,0x39,0x01,0xe1, + 0x32}; + static const uint8_t expectedTag[16] = { + 0xd7,0x63,0x60,0x3f,0x9a,0x3e,0x45,0x40,0x56,0x0b,0x15,0x87,0x5e,0x66,0x9f,0x99}; + + uint8_t ciphertext[sizeof(plaintext) - 1]; + uint8_t tag[16]; + if (ts::crypto::aeadIetfSeal(ciphertext, tag, key, nonce, aad, sizeof(aad), plaintext, sizeof(plaintext) - 1) && + equalBytes(ciphertext, expectedCiphertext, sizeof(ciphertext)) && + equalBytes(tag, expectedTag, sizeof(tag))) { + Serial.println("[PASS] ChaCha20-Poly1305 IETF vector"); + } else { + Serial.println("[FAIL] ChaCha20-Poly1305 IETF vector"); + } + + uint8_t opened[sizeof(ciphertext)]; + if (ts::crypto::aeadIetfOpen(opened, tag, key, nonce, aad, sizeof(aad), ciphertext, sizeof(ciphertext)) && + equalBytes(opened, plaintext, sizeof(opened))) { + Serial.println("[PASS] ChaCha20-Poly1305 decrypt"); + } else { + Serial.println("[FAIL] ChaCha20-Poly1305 decrypt"); + } + + ciphertext[0] ^= 1; + if (!ts::crypto::aeadIetfOpen(opened, tag, key, nonce, aad, sizeof(aad), ciphertext, sizeof(ciphertext))) { + Serial.println("[PASS] AEAD tamper rejection"); + } else { + Serial.println("[FAIL] AEAD tamper rejection"); + } + + uint8_t entropy[32]; + if (ts::crypto::randomBytes(entropy, sizeof(entropy))) { + uint8_t nonzero = 0; + for (uint8_t b : entropy) nonzero |= b; + Serial.println(nonzero ? "[PASS] Hardware entropy" : "[FAIL] Hardware entropy returned all zero"); + } else { + Serial.println("[FAIL] Hardware entropy API"); + } + + Serial.println("M1 self-test complete."); +} + +void loop() {} diff --git a/firmware/teensy41/include/blake2s.h b/firmware/teensy41/include/blake2s.h new file mode 100644 index 0000000..77f5206 --- /dev/null +++ b/firmware/teensy41/include/blake2s.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +int ts_blake2s(uint8_t *out, + size_t outlen, + const uint8_t *key, + size_t keylen, + const uint8_t *in, + size_t inlen); diff --git a/firmware/teensy41/include/ts_crypto.h b/firmware/teensy41/include/ts_crypto.h new file mode 100644 index 0000000..4181935 --- /dev/null +++ b/firmware/teensy41/include/ts_crypto.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +namespace ts::crypto { + +constexpr size_t kKeySize = 32; +constexpr size_t kHashSize = 32; +constexpr size_t kAeadTagSize = 16; +constexpr size_t kIetfNonceSize = 12; + +bool randomBytes(uint8_t *out, size_t length); + +bool x25519PublicKey(uint8_t publicKey[kKeySize], + const uint8_t privateKey[kKeySize]); + +bool x25519(uint8_t sharedSecret[kKeySize], + const uint8_t privateKey[kKeySize], + const uint8_t peerPublicKey[kKeySize]); + +bool blake2s(uint8_t digest[kHashSize], + const uint8_t *message, + size_t length); + +bool aeadIetfSeal(uint8_t *ciphertext, + uint8_t tag[kAeadTagSize], + const uint8_t key[kKeySize], + const uint8_t nonce[kIetfNonceSize], + const uint8_t *aad, + size_t aadLength, + const uint8_t *plaintext, + size_t plaintextLength); + +bool aeadIetfOpen(uint8_t *plaintext, + const uint8_t tag[kAeadTagSize], + const uint8_t key[kKeySize], + const uint8_t nonce[kIetfNonceSize], + const uint8_t *aad, + size_t aadLength, + const uint8_t *ciphertext, + size_t ciphertextLength); + +} // namespace ts::crypto diff --git a/firmware/teensy41/platformio.ini b/firmware/teensy41/platformio.ini index 5972d90..fceb501 100644 --- a/firmware/teensy41/platformio.ini +++ b/firmware/teensy41/platformio.ini @@ -6,6 +6,8 @@ monitor_speed = 115200 lib_deps = https://github.com/ssilverman/QNEthernet.git + https://github.com/LoupVaillant/Monocypher.git#4.0.3 build_flags = -DTS_TEENSY41_M0 + -DTS_TEENSY41_M1 diff --git a/firmware/teensy41/src/blake2s.cpp b/firmware/teensy41/src/blake2s.cpp new file mode 100644 index 0000000..d586122 --- /dev/null +++ b/firmware/teensy41/src/blake2s.cpp @@ -0,0 +1,143 @@ +#include "blake2s.h" + +#include + +namespace { + +constexpr uint32_t IV[8] = { + 0x6A09E667U, 0xBB67AE85U, 0x3C6EF372U, 0xA54FF53AU, + 0x510E527FU, 0x9B05688CU, 0x1F83D9ABU, 0x5BE0CD19U}; + +constexpr uint8_t SIGMA[10][16] = { + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + {14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3}, + {11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4}, + {7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8}, + {9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13}, + {2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9}, + {12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11}, + {13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10}, + {6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5}, + {10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 13, 12, 0}}; + +inline uint32_t rotr32(uint32_t value, unsigned count) { + return (value >> count) | (value << (32U - count)); +} + +inline uint32_t load32(const uint8_t *p) { + return static_cast(p[0]) | + (static_cast(p[1]) << 8) | + (static_cast(p[2]) << 16) | + (static_cast(p[3]) << 24); +} + +inline void store32(uint8_t *p, uint32_t value) { + p[0] = static_cast(value); + p[1] = static_cast(value >> 8); + p[2] = static_cast(value >> 16); + p[3] = static_cast(value >> 24); +} + +void compress(uint32_t h[8], const uint8_t block[64], uint32_t t0, + uint32_t t1, uint32_t finalFlag) { + uint32_t v[16]; + uint32_t m[16]; + for (int i = 0; i < 8; ++i) v[i] = h[i]; + for (int i = 0; i < 8; ++i) v[i + 8] = IV[i]; + for (int i = 0; i < 16; ++i) m[i] = load32(block + 4 * i); + + v[12] ^= t0; + v[13] ^= t1; + v[14] ^= finalFlag; + +#define G(a,b,c,d,x,y) do { \ + v[a] += v[b] + m[x]; \ + v[d] = rotr32(v[d] ^ v[a], 16); \ + v[c] += v[d]; \ + v[b] = rotr32(v[b] ^ v[c], 12); \ + v[a] += v[b] + m[y]; \ + v[d] = rotr32(v[d] ^ v[a], 8); \ + v[c] += v[d]; \ + v[b] = rotr32(v[b] ^ v[c], 7); \ + } while (0) + + for (int round = 0; round < 10; ++round) { + const uint8_t *s = SIGMA[round]; + G(0, 4, 8, 12, s[0], s[1]); + G(1, 5, 9, 13, s[2], s[3]); + G(2, 6, 10, 14, s[4], s[5]); + G(3, 7, 11, 15, s[6], s[7]); + G(0, 5, 10, 15, s[8], s[9]); + G(1, 6, 11, 12, s[10], s[11]); + G(2, 7, 8, 13, s[12], s[13]); + G(3, 4, 9, 14, s[14], s[15]); + } + +#undef G + + for (int i = 0; i < 8; ++i) h[i] ^= v[i] ^ v[i + 8]; +} + +} // namespace + +int ts_blake2s(uint8_t *out, size_t outlen, const uint8_t *key, + size_t keylen, const uint8_t *in, size_t inlen) { + if (out == nullptr || outlen == 0 || outlen > 32 || keylen > 32 || + (keylen != 0 && key == nullptr) || (inlen != 0 && in == nullptr)) { + return -1; + } + + uint32_t h[8]; + for (int i = 0; i < 8; ++i) h[i] = IV[i]; + h[0] ^= 0x01010000U | (static_cast(keylen) << 8) | + static_cast(outlen); + + uint32_t t0 = 0; + uint32_t t1 = 0; + uint8_t block[64] = {}; + size_t used = 0; + + if (keylen != 0) { + memcpy(block, key, keylen); + used = 64; + } + + if (used == 64) { + t0 += 64; + if (t0 == 0) ++t1; + compress(h, block, t0, t1, 0); + memset(block, 0, sizeof(block)); + used = 0; + } + + while (inlen != 0) { + const size_t take = (inlen > 64 - used) ? 64 - used : inlen; + memcpy(block + used, in, take); + used += take; + in += take; + inlen -= take; + + if (used == 64 && inlen != 0) { + t0 += 64; + if (t0 == 0) ++t1; + compress(h, block, t0, t1, 0); + memset(block, 0, sizeof(block)); + used = 0; + } + } + + const uint32_t previous = t0; + t0 += static_cast(used); + if (t0 < previous) ++t1; + memset(block + used, 0, 64 - used); + compress(h, block, t0, t1, 0xFFFFFFFFU); + + uint8_t full[32]; + for (int i = 0; i < 8; ++i) store32(full + 4 * i, h[i]); + memcpy(out, full, outlen); + + memset(block, 0, sizeof(block)); + memset(full, 0, sizeof(full)); + memset(h, 0, sizeof(h)); + return 0; +} diff --git a/firmware/teensy41/src/main.cpp b/firmware/teensy41/src/main.cpp index 793b3fe..ec11ad4 100644 --- a/firmware/teensy41/src/main.cpp +++ b/firmware/teensy41/src/main.cpp @@ -4,12 +4,61 @@ using namespace qindesign::network; EthernetUDP udp; +EthernetClient tcp; static void printAddress(const char *label, const IPAddress &address) { Serial.print(label); Serial.println(address); } +static bool waitForDhcp(uint32_t timeoutMs) { + const uint32_t start = millis(); + while (Ethernet.localIP() == INADDR_NONE && millis() - start < timeoutMs) { + Ethernet.maintain(); + delay(100); + } + return Ethernet.localIP() != INADDR_NONE; +} + +static bool testDns() { + IPAddress address; + if (!Ethernet.hostByName("example.com", address)) { + Serial.println("[FAIL] DNS lookup example.com"); + return false; + } + Serial.print("[PASS] DNS example.com -> "); + Serial.println(address); + return true; +} + +static bool testTcp() { + Serial.println("[TEST] TCP example.com:80"); + if (!tcp.connect("example.com", 80)) { + Serial.println("[FAIL] TCP connection"); + return false; + } + + tcp.println("GET / HTTP/1.1"); + tcp.println("Host: example.com"); + tcp.println("Connection: close"); + tcp.println(); + + const uint32_t start = millis(); + bool received = false; + while (millis() - start < 5000) { + Ethernet.maintain(); + if (tcp.available()) { + received = true; + break; + } + delay(10); + } + + tcp.stop(); + Serial.println(received ? "[PASS] TCP data received" : "[FAIL] TCP data timeout"); + return received; +} + void setup() { Serial.begin(115200); delay(1000); @@ -20,27 +69,19 @@ void setup() { Ethernet.begin(); - const uint32_t start = millis(); - while (!Ethernet.linkStatus() && millis() - start < 10000) { + const uint32_t linkStart = millis(); + while (!Ethernet.linkStatus() && millis() - linkStart < 10000) { delay(100); } - if (Ethernet.linkStatus()) { - Serial.println("[PASS] Ethernet link detected"); - } else { - Serial.println("[FAIL] Ethernet link not detected"); - } - - const uint32_t dhcpStart = millis(); - while (Ethernet.localIP() == INADDR_NONE && millis() - dhcpStart < 20000) { - Ethernet.maintain(); - delay(100); - } + Serial.println(Ethernet.linkStatus() + ? "[PASS] Ethernet link detected" + : "[FAIL] Ethernet link not detected"); - if (Ethernet.localIP() != INADDR_NONE) { - Serial.println("[PASS] IPv4 address acquired"); - } else { + if (!waitForDhcp(20000)) { Serial.println("[FAIL] DHCP did not provide an IPv4 address"); + } else { + Serial.println("[PASS] IPv4 address acquired"); } printAddress("IP: ", Ethernet.localIP()); @@ -55,7 +96,12 @@ void setup() { Serial.println("[FAIL] UDP socket initialization"); } - Serial.println("M0 hardware test ready."); + if (Ethernet.localIP() != INADDR_NONE) { + testDns(); + testTcp(); + } + + Serial.println("M0 network validation complete."); } void loop() { diff --git a/firmware/teensy41/src/ts_crypto.cpp b/firmware/teensy41/src/ts_crypto.cpp new file mode 100644 index 0000000..f90c957 --- /dev/null +++ b/firmware/teensy41/src/ts_crypto.cpp @@ -0,0 +1,89 @@ +#include "ts_crypto.h" +#include "blake2s.h" + +#include +#include +#include + +namespace ts::crypto { + +bool randomBytes(uint8_t *out, size_t length) { + if (out == nullptr && length != 0) return false; + for (size_t i = 0; i < length; ) { + const uint32_t value = Entropy.random(); + const size_t take = (length - i < sizeof(value)) ? length - i : sizeof(value); + for (size_t j = 0; j < take; ++j) { + out[i + j] = static_cast(value >> (8 * j)); + } + i += take; + } + return true; +} + +bool x25519PublicKey(uint8_t publicKey[kKeySize], + const uint8_t privateKey[kKeySize]) { + if (publicKey == nullptr || privateKey == nullptr) return false; + crypto_x25519_public_key(publicKey, privateKey); + return true; +} + +bool x25519(uint8_t sharedSecret[kKeySize], + const uint8_t privateKey[kKeySize], + const uint8_t peerPublicKey[kKeySize]) { + if (sharedSecret == nullptr || privateKey == nullptr || peerPublicKey == nullptr) { + return false; + } + crypto_x25519(sharedSecret, privateKey, peerPublicKey); + uint8_t nonzero = 0; + for (size_t i = 0; i < kKeySize; ++i) nonzero |= sharedSecret[i]; + return nonzero != 0; +} + +bool blake2s(uint8_t digest[kHashSize], const uint8_t *message, size_t length) { + return ts_blake2s(digest, kHashSize, nullptr, 0, message, length) == 0; +} + +bool aeadIetfSeal(uint8_t *ciphertext, + uint8_t tag[kAeadTagSize], + const uint8_t key[kKeySize], + const uint8_t nonce[kIetfNonceSize], + const uint8_t *aad, + size_t aadLength, + const uint8_t *plaintext, + size_t plaintextLength) { + if (ciphertext == nullptr || tag == nullptr || key == nullptr || nonce == nullptr || + (aadLength != 0 && aad == nullptr) || + (plaintextLength != 0 && plaintext == nullptr)) { + return false; + } + + crypto_aead_ctx ctx; + crypto_aead_init_ietf(&ctx, key, nonce); + crypto_aead_write(&ctx, ciphertext, tag, aad, aadLength, plaintext, plaintextLength); + crypto_wipe(&ctx, sizeof(ctx)); + return true; +} + +bool aeadIetfOpen(uint8_t *plaintext, + const uint8_t tag[kAeadTagSize], + const uint8_t key[kKeySize], + const uint8_t nonce[kIetfNonceSize], + const uint8_t *aad, + size_t aadLength, + const uint8_t *ciphertext, + size_t ciphertextLength) { + if (plaintext == nullptr || tag == nullptr || key == nullptr || nonce == nullptr || + (aadLength != 0 && aad == nullptr) || + (ciphertextLength != 0 && ciphertext == nullptr)) { + return false; + } + + crypto_aead_ctx ctx; + crypto_aead_init_ietf(&ctx, key, nonce); + const bool ok = crypto_aead_read(&ctx, plaintext, tag, aad, aadLength, + ciphertext, ciphertextLength) == 0; + crypto_wipe(&ctx, sizeof(ctx)); + return ok; +} + +} // namespace ts::crypto diff --git a/firmware/teensy41/test/M1-CRYPTO.md b/firmware/teensy41/test/M1-CRYPTO.md new file mode 100644 index 0000000..a874472 --- /dev/null +++ b/firmware/teensy41/test/M1-CRYPTO.md @@ -0,0 +1,32 @@ +# M1 crypto verification + +M1 validates the primitives required by the later Tailscale/WireGuard implementation. + +## Implemented + +- X25519 via Monocypher 4.0.3 +- ChaCha20-Poly1305 with the IETF 96-bit nonce form via Monocypher 4.0.3 +- BLAKE2s-256 implemented locally from RFC 7693 +- Teensy 4.1 hardware entropy through the Entropy library +- all-zero X25519 shared-secret rejection +- secret context wiping after AEAD operations + +## Deterministic tests + +`m1_vectors.cpp` contains deterministic known-answer checks for: + +- RFC 7748 X25519 public keys and shared secret +- BLAKE2s-256 of `abc` +- ChaCha20-Poly1305 IETF encryption/decryption +- ciphertext tamper rejection +- entropy API availability + +`blake2s_host.cpp` runs the BLAKE2s implementation independently on the host and checks empty, `abc`, and keyed `abc` outputs. + +## Physical Teensy test + +`examples/m1_crypto/m1_crypto.ino` exercises X25519, BLAKE2s, ChaCha20-Poly1305, tamper rejection, and hardware entropy on the actual Teensy 4.1. + +Run at 115200 baud and require every primitive to report `[PASS]` before continuing to M2. + +No Tailscale auth key, node private key, machine private key, or other production credential belongs in this directory or CI logs. diff --git a/firmware/teensy41/test/blake2s_host.cpp b/firmware/teensy41/test/blake2s_host.cpp new file mode 100644 index 0000000..bf3586d --- /dev/null +++ b/firmware/teensy41/test/blake2s_host.cpp @@ -0,0 +1,41 @@ +#include +#include +#include +#include + +#include "blake2s.h" + +static bool expectHex(const uint8_t *actual, const char *expected) { + char hex[65] = {}; + for (size_t i = 0; i < 32; ++i) { + std::snprintf(hex + i * 2, 3, "%02x", actual[i]); + } + return std::strcmp(hex, expected) == 0; +} + +int main() { + uint8_t digest[32]; + const uint8_t abc[] = {'a', 'b', 'c'}; + + if (ts_blake2s(digest, 32, nullptr, 0, nullptr, 0) != 0 || + !expectHex(digest, "69217a3079908094e11121d042354a7c1f55b6482ca1a51e1b250dfd1ed0eef9")) { + std::fprintf(stderr, "BLAKE2s empty vector failed\n"); + return 1; + } + + if (ts_blake2s(digest, 32, nullptr, 0, abc, sizeof(abc)) != 0 || + !expectHex(digest, "508c5e8c327c14e2e1a72ba34eeb452f37458b209ed63a294d999b4c86675982")) { + std::fprintf(stderr, "BLAKE2s abc vector failed\n"); + return 2; + } + + const uint8_t key[] = {'k', 'e', 'y'}; + if (ts_blake2s(digest, 32, key, sizeof(key), abc, sizeof(abc)) != 0 || + !expectHex(digest, "3f9723437b033bf0c1f4df43cafd0776068cb0a95912de13f3b2952a3aba764d")) { + std::fprintf(stderr, "BLAKE2s keyed vector failed\n"); + return 3; + } + + std::puts("BLAKE2s host tests: PASS"); + return 0; +} diff --git a/firmware/teensy41/test/m1_vectors.cpp b/firmware/teensy41/test/m1_vectors.cpp new file mode 100644 index 0000000..3cf076d --- /dev/null +++ b/firmware/teensy41/test/m1_vectors.cpp @@ -0,0 +1,88 @@ +#include +#include +#include + +#include "ts_crypto.h" + +static bool equal(const uint8_t *a, const uint8_t *b, size_t n) { + return memcmp(a, b, n) == 0; +} + +static void hex(const char *name, const uint8_t *v, size_t n) { + printf("%s=", name); + for (size_t i = 0; i < n; ++i) printf("%02x", v[i]); + puts(""); +} + +int main() { + static const uint8_t aliceSk[32] = { + 0x77,0x07,0x6d,0x0a,0x73,0x18,0xa5,0x7d,0x3c,0x16,0xc1,0x72,0x51,0xb2,0x66,0x45, + 0xdf,0x4c,0x2f,0x87,0xeb,0xc0,0x99,0x2a,0xb1,0x77,0xfb,0xa5,0x1d,0xb9,0x2c,0x2a}; + static const uint8_t alicePkExpected[32] = { + 0x85,0x20,0xf0,0x09,0x89,0x30,0xa7,0x54,0x74,0x8b,0x7d,0xdc,0xb4,0x3e,0xf7,0x5a, + 0x0d,0xbf,0x3a,0x0d,0x26,0x38,0x1a,0xf4,0xeb,0xa4,0xa9,0x8e,0xaa,0x9b,0x4e,0x6a}; + static const uint8_t bobSk[32] = { + 0x5d,0xab,0x08,0x7e,0x62,0x4a,0x8a,0x4b,0x79,0xe1,0x7f,0x8b,0x83,0x80,0x0e,0xe6, + 0x6f,0x3b,0xb1,0x29,0x26,0x18,0xb6,0xfd,0x1c,0x2f,0x8b,0x27,0xff,0x88,0xe0,0xeb}; + static const uint8_t bobPkExpected[32] = { + 0xde,0x9e,0xdb,0x7d,0x7b,0x7d,0xc1,0xb4,0xd3,0x5b,0x61,0xc2,0xec,0xe4,0x35,0x37, + 0x3f,0x83,0x43,0xc8,0x5b,0x78,0x67,0x4d,0xad,0xfc,0x7e,0x14,0x6f,0x88,0x2b,0x4f}; + static const uint8_t sharedExpected[32] = { + 0x4a,0x5d,0x9d,0x5b,0xa4,0xce,0x2d,0xe1,0x72,0x8e,0x3b,0xf4,0x80,0x35,0x0f,0x25, + 0xe0,0x7e,0x21,0xc9,0x47,0xd1,0x9e,0x33,0x76,0xf0,0x9b,0x3c,0x1e,0x16,0x17,0x42}; + + uint8_t pk[32]; + uint8_t shared[32]; + if (!ts::crypto::x25519PublicKey(pk, aliceSk) || !equal(pk, alicePkExpected, 32)) return 1; + if (!ts::crypto::x25519PublicKey(pk, bobSk) || !equal(pk, bobPkExpected, 32)) return 2; + if (!ts::crypto::x25519(shared, aliceSk, bobPkExpected) || !equal(shared, sharedExpected, 32)) return 3; + + const uint8_t abc[] = {'a','b','c'}; + const uint8_t blakeExpected[32] = { + 0x50,0x8c,0x5e,0x8c,0x32,0x7c,0x14,0xe2,0xe1,0xa7,0x2b,0xa3,0x4e,0xeb,0x45,0x2f, + 0x37,0x45,0x8b,0x20,0x9e,0xd6,0x3a,0x29,0x4d,0x99,0x9b,0x4c,0x86,0x67,0x59,0x82}; + uint8_t digest[32]; + if (!ts::crypto::blake2s(digest, abc, sizeof(abc)) || !equal(digest, blakeExpected, 32)) return 4; + + static const uint8_t key[32] = { + 0x80,0x81,0x82,0x83,0x84,0x85,0x86,0x87,0x88,0x89,0x8a,0x8b,0x8c,0x8d,0x8e,0x8f, + 0x90,0x91,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0x9b,0x9c,0x9d,0x9e,0x9f}; + static const uint8_t nonce[12] = {0x40,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x07,0,0,0}; + static const uint8_t aad[12] = {0x50,0x51,0x52,0x53,0xc0,0xc1,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7}; + static const uint8_t plaintext[] = "Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it."; + static const uint8_t ciphertextExpected[114] = { + 0x9a,0xbc,0x18,0xdb,0x71,0x32,0xd3,0x04,0xf6,0x37,0xd6,0x4e,0x40,0x45,0x8a,0x92, + 0x07,0x8d,0xd4,0x9a,0x1d,0x4f,0xbc,0xcb,0x78,0x10,0xd9,0x60,0x1e,0xb3,0xdc,0xd5,0xd3, + 0xa8,0x9a,0x67,0x95,0xaa,0x8b,0x76,0xcc,0x00,0x7c,0x0e,0x24,0x5b,0x0c,0x18,0x72, + 0xd1,0xa5,0x00,0x3c,0x0e,0xb2,0x36,0x4a,0xfa,0x99,0xed,0xc4,0x51,0xb7,0xa6,0xfb, + 0xec,0x73,0x62,0x36,0xf0,0xa9,0x2e,0xbb,0x8a,0xb3,0x5e,0x20,0x81,0x89,0x4c,0xea, + 0x3b,0xc0,0x6c,0x33,0x97,0xb9,0x79,0xdb,0xcd,0x44,0x5f,0x45,0xb3,0x7c,0x4c,0xad, + 0x2b,0x60,0xe1,0x80,0xa6,0x42,0xfd,0xe7,0x20,0x37,0x48,0x03,0x4c,0x39,0x01,0xe1, + 0x32}; + static const uint8_t tagExpected[16] = { + 0xd7,0x63,0x60,0x3f,0x9a,0x3e,0x45,0x40,0x56,0x0b,0x15,0x87,0x5e,0x66,0x9f,0x99}; + + uint8_t ciphertext[sizeof(plaintext) - 1]; + uint8_t tag[16]; + if (!ts::crypto::aeadIetfSeal(ciphertext, tag, key, nonce, aad, sizeof(aad), plaintext, + sizeof(plaintext) - 1)) return 5; + if (!equal(ciphertext, ciphertextExpected, sizeof(ciphertext)) || !equal(tag, tagExpected, 16)) return 6; + + uint8_t opened[sizeof(ciphertext)]; + if (!ts::crypto::aeadIetfOpen(opened, tag, key, nonce, aad, sizeof(aad), ciphertext, sizeof(ciphertext))) return 7; + if (!equal(opened, plaintext, sizeof(opened))) return 8; + + ciphertext[0] ^= 1; + if (ts::crypto::aeadIetfOpen(opened, tag, key, nonce, aad, sizeof(aad), ciphertext, sizeof(ciphertext))) return 9; + + uint8_t entropy[32]; + if (!ts::crypto::randomBytes(entropy, sizeof(entropy))) return 10; + uint8_t orAll = 0; + for (uint8_t b : entropy) orAll |= b; + if (orAll == 0) return 11; + + hex("alice-public", alicePkExpected, 32); + hex("shared", sharedExpected, 32); + puts("M1 PASS"); + return 0; +}