Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 25 additions & 0 deletions .github/workflows/teensy41.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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: Build Teensy 4.1 firmware
working-directory: firmware/teensy41
run: pio run -e teensy41
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.
18 changes: 18 additions & 0 deletions firmware/teensy41/include/ts_crypto.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#pragma once

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

namespace ts::crypto {

constexpr size_t kKeySize = 32;
constexpr size_t kHashSize = 32;

// M1 interface. The implementation will be backed by an audited crypto
// library after host and Teensy test vectors are established.
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);

} // namespace ts::crypto
80 changes: 63 additions & 17 deletions firmware/teensy41/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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());
Expand All @@ -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() {
Expand Down
Loading