Skip to content

Commit

Permalink
Add TinyGo support; Add Cloudflare Workers rendezvous server example
Browse files Browse the repository at this point in the history
Requires the TinyGo fork found at https://github.com/ben-krieger/tinygo/tree/reflect-clear-and-more

Signed-off-by: Ben Krieger <[email protected]>
  • Loading branch information
ben-krieger committed Oct 28, 2024
1 parent 1fef581 commit aaa9fe1
Show file tree
Hide file tree
Showing 11 changed files with 513 additions and 91 deletions.
9 changes: 2 additions & 7 deletions cbor/cbor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1243,15 +1243,10 @@ func (e *Encoder) encodeTextOrBinary(rv reflect.Value) error {

// Unaddressable arrays cannot be made into slices, so we must create a
// slice and copy contents into it
slice := reflect.MakeSlice(
reflect.SliceOf(rv.Type().Elem()),
rv.Len(),
rv.Len(),
)
if n := reflect.Copy(slice, rv); n != rv.Len() {
b = make([]byte, rv.Len())
if n := reflect.Copy(reflect.ValueOf(b), rv); n != rv.Len() {
panic("array contents were not fully copied into a slice for encoding")
}
b = slice.Bytes()
}

info := u64Bytes(uint64(len(b)))
Expand Down
1 change: 1 addition & 0 deletions examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/fido-device-onboard/go-fdo/tpm v0.0.0-00010101000000-000000000000
github.com/google/go-tpm v0.9.2-0.20240920144513-364d5f2f78b9
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba
github.com/syumai/workers v0.26.3
hermannm.dev/devlog v0.4.1
)

Expand Down
2 changes: 2 additions & 0 deletions examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/syumai/workers v0.26.3 h1:AF+IBaRccbR4JIj2kNJLJblruPFMD/pAbzkopejGcP8=
github.com/syumai/workers v0.26.3/go.mod h1:ZnqmdiHNBrbxOLrZ/HJ5jzHy6af9cmiNZk10R9NrIEA=
github.com/tetratelabs/wazero v1.8.1 h1:NrcgVbWfkWvVc4UtT4LRLDf91PsOzDzefMdwhLfA550=
github.com/tetratelabs/wazero v1.8.1/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
Expand Down
147 changes: 147 additions & 0 deletions examples/wasm/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// SPDX-FileCopyrightText: (C) 2024 Intel Corporation
// SPDX-License-Identifier: Apache 2.0

//go:build tinygo

// Package main implements a Rendezvous Server which can be compiled with
// TinyGo and run on Cloudflare Workers within the free tier (under reasonable
// load).
//
// Create a Cloudflare Worker with the name "rv" and start a new repo with the
// following configuration:
//
// wrangler.toml:
//
// name = "rv"
// main = "./build/worker.mjs"
//
// [build]
// command = """
// go run github.com/syumai/workers/cmd/[email protected] -mode tinygo &&
// tinygo build -o ./build/app.wasm -target wasm -no-debug ./main.go
// """
//
// [observability]
// enabled = true
//
// [[ d1_databases ]]
// binding = "RendezvousDB"
// database_name = "rv"
// database_id = "COPY_YOUR_UUID_HERE"
//
// Upon creating a D1 database instance with name "rv" and updating
// wrangler.toml, execute the following schema.sql file using:
//
// $ wrangler d1 execute rv --remote --file=./schema.sql
//
// schema.sql:
//
// PRAGMA foreign_keys = ON;
// CREATE TABLE IF NOT EXISTS sessions
// ( id BLOB PRIMARY KEY
// , protocol INTEGER NOT NULL
// );
// CREATE TABLE IF NOT EXISTS to0_sessions
// ( session BLOB UNIQUE NOT NULL
// , nonce BLOB
// , FOREIGN KEY(session) REFERENCES sessions(id) ON DELETE CASCADE
// );
// CREATE TABLE IF NOT EXISTS to1_sessions
// ( session BLOB UNIQUE NOT NULL
// , nonce BLOB
// , alg INTEGER
// , FOREIGN KEY(session) REFERENCES sessions(id) ON DELETE CASCADE
// );
// CREATE TABLE IF NOT EXISTS rv_blobs
// ( guid BLOB PRIMARY KEY
// , rv BLOB NOT NULL
// , voucher BLOB NOT NULL
// , exp INTEGER NOT NULL
// );
// CREATE TABLE IF NOT EXISTS trusted_emails
// ( email TEXT PRIMARY KEY
// );
// CREATE TABLE IF NOT EXISTS trusted_owners
// ( pkix BLOB PRIMARY KEY
// , email TEXT NOT NULL
// , FOREIGN KEY(email) REFERENCES trusted_emails(email) ON DELETE CASCADE
// );
//
// Add users by executing SQL with "wrangler d1 execute rv --remote" to add to
// the trusted_emails table. Then, add owner keys for users in the
// trusted_owners table.
//
// Setting up Cloudflare cron jobs to remove expired rendezvous blobs is left
// as an exercise for the implementer.
package main

import (
"context"
"crypto/x509"
"database/sql"
"errors"
"fmt"
"log/slog"
"net/http"
"os"

"github.com/syumai/workers"
_ "github.com/syumai/workers/cloudflare/d1"

"github.com/fido-device-onboard/go-fdo"
fdo_http "github.com/fido-device-onboard/go-fdo/http"
"github.com/fido-device-onboard/go-fdo/sqlite"
)

const oneWeekInSeconds uint32 = 7 * 24 * 60 * 60

func main() {
db, err := sql.Open("d1", "RendezvousDB")
if err != nil {
slog.Error("d1 connect", "error", err)
os.Exit(1)
}
state := sqlite.New(db)

// If building with Go instead of TinyGo, use:
// (*http.ServeMux).Handle("POST /fdo/101/{msg}", ...)
handler := http.NewServeMux()
handler.Handle("/fdo/101/", &fdo_http.Handler{
TO0Responder: &fdo.TO0Server{
Session: state,
RVBlobs: state,
AcceptVoucher: func(ctx context.Context, ov fdo.Voucher) (accept bool, err error) {
owner, err := ov.OwnerPublicKey()
if err != nil {
return false, fmt.Errorf("error getting voucher owner key: %w", err)
}
der, err := x509.MarshalPKIXPublicKey(owner)
if err != nil {
return false, fmt.Errorf("error marshaling voucher owner key: %w", err)
}
return trustedOwner(ctx, db, der)
},
NegotiateTTL: func(requestedSeconds uint32, ov fdo.Voucher) (waitSeconds uint32) {
return min(requestedSeconds, oneWeekInSeconds)
},
},
TO1Responder: &fdo.TO1Server{
Session: state,
RVBlobs: state,
},
})
workers.Serve(handler)
}

func trustedOwner(ctx context.Context, db *sql.DB, pkixKey []byte) (bool, error) {
var email string
row := db.QueryRowContext(ctx, `SELECT email FROM trusted_owners WHERE pkix = ?`, pkixKey)
if err := row.Scan(&email); errors.Is(err, sql.ErrNoRows) {
return false, nil
} else if err != nil {
return false, err
}
slog.Info("accepting voucher", "user", email)

return true, nil
}
16 changes: 16 additions & 0 deletions http/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/hex"
"log/slog"

"github.com/fido-device-onboard/go-fdo/cbor"
"github.com/fido-device-onboard/go-fdo/cbor/cdn"
)

Expand All @@ -22,3 +23,18 @@ func tryDebugNotation(b []byte) string {
}
return d
}

func debugUnencryptedMessage(msgType uint8, msg any) {
if debugEnabled() {
return
}
body, _ := cbor.Marshal(msg)
slog.Debug("unencrypted request", "msg", msgType, "body", tryDebugNotation(body))
}

func debugDecryptedMessage(msgType uint8, decrypted []byte) {
if debugEnabled() {
return
}
slog.Debug("decrypted response", "msg", msgType, "body", tryDebugNotation(decrypted))
}
39 changes: 5 additions & 34 deletions http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import (
"io"
"log/slog"
"net/http"
"net/http/httptest"
"net/http/httputil"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -42,12 +40,10 @@ type Handler struct {

func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Parse message type from request URL
typ, err := strconv.ParseUint(r.PathValue("msg"), 10, 8)
if err != nil {
writeErr(w, 0, fmt.Errorf("invalid message type"))
msgType, ok := msgTypeFromPath(w, r)
if !ok {
return
}
msgType := uint8(typ)
proto := protocol.Of(msgType)

// Parse request headers
Expand Down Expand Up @@ -108,39 +104,14 @@ func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}

if debugEnabled() {
h.debugRequest(ctx, w, r, msgType, resp)
debugRequest(w, r, func(w http.ResponseWriter, r *http.Request) {
h.handleRequest(ctx, w, r, msgType, resp)
})
return
}
h.handleRequest(ctx, w, r, msgType, resp)
}

func (h Handler) debugRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, msgType uint8, resp protocol.Responder) {
// Dump request
debugReq, _ := httputil.DumpRequest(r, false)
var saveBody bytes.Buffer
if _, err := saveBody.ReadFrom(r.Body); err == nil {
r.Body = io.NopCloser(&saveBody)
}
slog.Debug("request", "dump", string(bytes.TrimSpace(debugReq)),
"body", tryDebugNotation(saveBody.Bytes()))

// Dump response
rr := httptest.NewRecorder()
h.handleRequest(ctx, rr, r, msgType, resp)
debugResp, _ := httputil.DumpResponse(rr.Result(), false)
slog.Debug("response", "dump", string(bytes.TrimSpace(debugResp)),
"body", tryDebugNotation(rr.Body.Bytes()))

// Copy recorded response into response writer
for key, values := range rr.Header() {
for _, value := range values {
w.Header().Add(key, value)
}
}
w.WriteHeader(rr.Code)
_, _ = w.Write(rr.Body.Bytes())
}

func (h Handler) handleRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, msgType uint8, resp protocol.Responder) {
// Validate content length
maxSize := h.MaxContentLength
Expand Down
30 changes: 4 additions & 26 deletions http/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import (
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httputil"
"net/url"
"path"
"strconv"
Expand Down Expand Up @@ -51,8 +49,6 @@ type Transport struct {
}

// Send sends a single message and receives a single response message.
//
//nolint:gocyclo
func (t *Transport) Send(ctx context.Context, msgType uint8, msg any, sess kex.Session) (respType uint8, _ io.ReadCloser, _ error) {
// Initialize default values
if t.Client == nil {
Expand All @@ -64,10 +60,7 @@ func (t *Transport) Send(ctx context.Context, msgType uint8, msg any, sess kex.S

// Encrypt if a key exchange session is provided
if sess != nil {
if debugEnabled() {
body, _ := cbor.Marshal(msg)
slog.Debug("unencrypted request", "msg", msgType, "body", tryDebugNotation(body))
}
debugUnencryptedMessage(msgType, msg)
var err error
msg, err = sess.Encrypt(rand.Reader, msg)
if err != nil {
Expand Down Expand Up @@ -105,24 +98,12 @@ func (t *Transport) Send(ctx context.Context, msgType uint8, msg any, sess kex.S
}

// Perform HTTP request
if debugEnabled() {
debugReq, _ := httputil.DumpRequestOut(req, false)
slog.Debug("request", "dump", string(bytes.TrimSpace(debugReq)),
"body", tryDebugNotation(body.Bytes()))
}
debugRequestOut(req, body)
resp, err := t.Client.Do(req)
if err != nil {
return 0, nil, fmt.Errorf("error making HTTP request for message %d: %w", msgType, err)
}
if debugEnabled() {
debugResp, _ := httputil.DumpResponse(resp, false)
var saveBody bytes.Buffer
if _, err := saveBody.ReadFrom(resp.Body); err == nil {
resp.Body = io.NopCloser(&saveBody)
}
slog.Debug("response", "dump", string(bytes.TrimSpace(debugResp)),
"body", tryDebugNotation(saveBody.Bytes()))
}
debugResponse(resp)

return t.handleResponse(resp, sess)
}
Expand Down Expand Up @@ -189,10 +170,7 @@ func (t *Transport) handleResponse(resp *http.Response, sess kex.Session) (msgTy
if err != nil {
return 0, nil, fmt.Errorf("error decrypting message %d: %w", msgType, err)
}

if debugEnabled() {
slog.Debug("decrypted response", "msg", msgType, "body", tryDebugNotation(decrypted))
}
debugDecryptedMessage(msgType, decrypted)

content = io.NopCloser(bytes.NewBuffer(decrypted))
}
Expand Down
Loading

0 comments on commit aaa9fe1

Please sign in to comment.