Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/teensy41.yml
Original file line number Diff line number Diff line change
@@ -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
55 changes: 55 additions & 0 deletions docs/superpowers/plans/2026-08-12-teensy41-tailscale-m1-crypto.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions firmware/teensy41/docs/m1-crypto.md
Original file line number Diff line number Diff line change
@@ -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.
106 changes: 106 additions & 0 deletions firmware/teensy41/examples/m1_crypto/m1_crypto.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#include <Arduino.h>
#include <string.h>

#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() {}
11 changes: 11 additions & 0 deletions firmware/teensy41/include/blake2s.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#pragma once

#include <stddef.h>
#include <stdint.h>

int ts_blake2s(uint8_t *out,
size_t outlen,
const uint8_t *key,
size_t keylen,
const uint8_t *in,
size_t inlen);
44 changes: 44 additions & 0 deletions firmware/teensy41/include/ts_crypto.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#pragma once

#include <stddef.h>
#include <stdint.h>

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
2 changes: 2 additions & 0 deletions firmware/teensy41/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading