initial commit

main
Bill Mill 2 years ago
commit 8c88c79fd9
  1. 1
      .gitignore
  2. 131
      client/main.go
  3. 5
      go.mod
  4. 2
      go.sum
  5. 3
      modd.conf
  6. 219
      server/main.go

1
.gitignore vendored

@ -0,0 +1 @@
*.db

@ -0,0 +1,131 @@
package main
import (
"bytes"
"crypto/ed25519"
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"os/user"
"path/filepath"
"time"
)
func validKey() (ed25519.PublicKey, ed25519.PrivateKey) {
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
panic(err)
}
// TODO: generate a new key if necessary
target, err := hex.DecodeString("ed2023")
if err != nil {
panic(err)
}
for bytes.Compare(pub[29:32], target) != 0 {
// fmt.Printf("%x\n", pub[29:32])
pub, priv, err = ed25519.GenerateKey(nil)
if err != nil {
panic(err)
}
}
fmt.Printf("%s\n", fmt.Sprintf("%x", pub))
return pub, priv
}
func fileExists(name string) bool {
if _, err := os.Stat(name); errors.Is(err, os.ErrNotExist) {
return false
}
return true
}
func getKeys() (ed25519.PublicKey, ed25519.PrivateKey) {
user, err := user.Current()
if err != nil {
panic(err)
}
configPath := os.Getenv("XDG_CONFIG_HOME")
if configPath == "" {
configPath = filepath.Join(user.HomeDir, ".config", "spring83")
}
if err = os.MkdirAll(configPath, os.ModePerm); err != nil {
panic(err)
}
pubfile := filepath.Join(configPath, "key.pub")
privfile := filepath.Join(configPath, "key.priv")
var pubkey ed25519.PublicKey
var privkey ed25519.PrivateKey
if fileExists(pubfile) && fileExists(privfile) {
pubkey, err = ioutil.ReadFile(pubfile)
if err != nil {
panic(err)
}
privkey, err = ioutil.ReadFile(privfile)
if err != nil {
panic(err)
}
} else {
fmt.Printf("Generating key, give this a minute or two\n")
pubkey, privkey = validKey()
os.WriteFile(pubfile, pubkey, 0666)
os.WriteFile(privfile, privkey, 0600)
}
return pubkey, privkey
}
func main() {
pubkey, privkey := getKeys()
// initialize http client
client := &http.Client{}
body, err := ioutil.ReadAll(os.Stdin)
if err != nil {
panic(err)
}
if len(body) == 0 {
panic(fmt.Errorf("input required"))
}
if len(body) > 2217 {
panic(fmt.Errorf("input body too long"))
}
// set the HTTP method, url, and request body
url := fmt.Sprintf("http://localhost:8000/%x", pubkey)
fmt.Printf("URL: %s\n", url)
req, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(body))
if err != nil {
panic(err)
}
sig := ed25519.Sign(privkey, body)
fmt.Printf("Spring-83 Signature=%x\n", sig)
req.Header.Set("Authorization", fmt.Sprintf("Spring-83 Signature=%x", sig))
dt := time.Now().Format(time.RFC1123)
req.Header.Set("If-Unmodified-Since", dt)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
responseBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Printf("%s: %s\n", resp.Status, responseBody)
}

@ -0,0 +1,5 @@
module github.com/llimllib/springer
go 1.18
require github.com/mattn/go-sqlite3 v1.14.13 // indirect

@ -0,0 +1,2 @@
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=

@ -0,0 +1,3 @@
server/main.go {
daemon: go run server/main.go
}

@ -0,0 +1,219 @@
// https://github.com/robinsloan/spring-83-spec/blob/main/draft-20220609.md
package main
import (
"bytes"
"crypto/ed25519"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
)
func must(err error) {
if err != nil {
log.Fatal(err)
}
}
func initDB() *sql.DB {
dbName := "./spring83.db"
// if the db doesn't exist, create it
if _, err := os.Stat(dbName); errors.Is(err, os.ErrNotExist) {
log.Printf("initializing new database")
db, err := sql.Open("sqlite3", dbName)
must(err)
initSQL := `
CREATE TABLE boards (
key text NOT NULL PRIMARY KEY,
board text,
expiry text
);
`
_, err = db.Exec(initSQL)
if err != nil {
log.Fatal("%q: %s\n", err, initSQL)
}
return db
}
db, err := sql.Open("sqlite3", dbName)
must(err)
return db
}
func main() {
db := initDB()
log.Print("starting helloserver")
server := newSpring83Server(db)
http.HandleFunc("/", server.RootHandler)
log.Fatal(http.ListenAndServe(":8000", nil))
}
type Spring83Server struct {
db *sql.DB
}
func newSpring83Server(db *sql.DB) *Spring83Server {
return &Spring83Server{
db: db,
}
}
func (s *Spring83Server) publishBoard(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
if len(body) > 2217 {
http.Error(w, "Payload too large", http.StatusRequestEntityTooLarge)
return
}
var mtime time.Time
if ifUnmodifiedHeader, ok := r.Header["If-Unmodified-Since"]; !ok {
http.Error(w, "Missing If-Unmodified-Since header", http.StatusBadRequest)
return
} else {
// spec says "in HTTP format", but it's not entirely clear if this matches?
if mtime, err = time.Parse(time.RFC1123, ifUnmodifiedHeader[0]); err != nil {
http.Error(w, "Invalid format for If-Unmodified-Since header", http.StatusBadRequest)
return
}
}
// TODO : If this value (mtime) is older or equal to the timestamp of the
// server's version of the board, the server must reject the request with
// 409 Conflict.
//
// this is just here to allow the variable to exist
log.Printf("%s\n", mtime)
var signature []byte
if authorizationHeaders, ok := r.Header["Authorization"]; !ok {
http.Error(w, "Missing Authorization header", http.StatusBadRequest)
return
} else {
parts := strings.Split(authorizationHeaders[0], " ")
if parts[0] != "Spring-83" || len(parts) < 2 {
http.Error(w, "Invalid Authorization Type", http.StatusBadRequest)
return
}
sig := strings.Split(parts[1], "=")
if len(sig) < 1 {
http.Error(w, "Invalid Signature", http.StatusBadRequest)
return
}
sigString := sig[1]
if len(sigString) != 128 {
http.Error(w, fmt.Sprintf("Expecting 64-bit signature %s %d", sigString, len(sigString)), http.StatusBadRequest)
return
}
signature, err = hex.DecodeString(sigString)
if err != nil {
http.Error(w, "Unable to decode signature", http.StatusBadRequest)
return
}
}
// Spring '83 specifies a test keypair
// Servers must not accept PUTs for this key, returning 401 Unauthorized.
// The server may also use a denylist to block certain keys, rejecting all PUTs for those keys.
denylist := []string{"fad415fbaa0339c4fd372d8287e50f67905321ccfd9c43fa4c20ac40afed1983"}
for _, key := range denylist {
if bytes.Compare(signature, []byte(key)) == 0 {
http.Error(w, "Denied", http.StatusUnauthorized)
}
}
// the path must be a ed25519 public key of 32 bytes
fmt.Printf("%s\n", r.URL.Path[1:])
key, err := hex.DecodeString(r.URL.Path[1:])
if err != nil || len(key) != 32 {
http.Error(w, "Invalid key", http.StatusBadRequest)
return
}
// If the current four-digit year is YYYY, and the
// previous four-digit year is YYYX, the server must
// only accept PUTs for keys that end with the four
// digits YYYY or YYYX, preceded in turn by the two hex
// digits "ed". This is the years-of-use requirement.
//
// The server must reject other keys with 400 Bad
// Request.
keyStr := fmt.Sprintf("%x", key)
last4 := string(keyStr[60:64])
if keyStr[58:60] != "ed" ||
(last4 != time.Now().Format("2006") &&
last4 != time.Now().AddDate(1, 0, 0).Format("2006")) {
log.Printf("%s %s %s", keyStr[58:60] == "ed", last4 == time.Now().Format("2006"), time.Now().Format("2006"))
http.Error(w, "Signature must end with edYYYY", http.StatusBadRequest)
return
}
// at this point, we should have met all the preconditions prior to the
// cryptographic check. By the spec, we should perform all
// non-cryptographic checks first.
if !ed25519.Verify(key, body, signature) {
http.Error(w, "Invalid signature", http.StatusBadRequest)
return
}
// TODO:
// Additionally, if the server doesn't have any board
// stored for <key>, then it must apply another check. The
// key, interpreted as a 256-bit number, must be less than
// a threshold defined by the server's difficulty factor:
//
// MAX_SIG = (2**256 - 1) key_threshold = MAX_SIG * ( 1.0 -
// difficulty_factor)
//
// This check is not applied to keys for which the server
// already has a board stored. You can read more about the
// difficulty factor later in this document.
expiry := time.Now().AddDate(0, 0, 7).Format(time.RFC3339)
_, err = s.db.Exec(`
INSERT INTO boards (key, board, expiry)
values(?, ?, ?)
ON CONFLICT(key) DO UPDATE SET
board=?,
expiry=?
`, keyStr, body, expiry, body, expiry)
if err != nil {
log.Printf("%s", err)
http.Error(w, "Server error", http.StatusInternalServerError)
}
}
func (s *Spring83Server) showBoard(w http.ResponseWriter, r *http.Request) {
}
func (s *Spring83Server) RootHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "PUT" {
s.publishBoard(w, r)
} else if r.Method == "GET" {
s.showBoard(w, r)
} else {
http.Error(w, "Invalid method", http.StatusBadRequest)
}
}
Loading…
Cancel
Save