💾 Archived View for thrig.me › blog › 2023 › 05 › 10 › misfin.go captured on 2023-05-24 at 18:08:48.

View Raw

More Information

-=-=-=-=-=-=-

// misfin - a prototype misfin client in Go
//
//   go build misfin.go
//
// then see misfind.go for how to generate a certificate; I'm using the
// same certificate for both the server and the client:
//
//   echo "some message here" | ./misfin usercert.pem ed@example.org

package main

import (
	"bufio"
	"crypto/tls"
	"fmt"
	"io"
	"log"
	"os"
	"regexp"
	"strings"
	// optional, adds pledge on OpenBSD
	"suah.dev/protect"
)

func main() {
	if len(os.Args) != 3 {
		fmt.Fprintln(os.Stderr, "Usage: usercert.pem misfin://[user]@[hostname] < ...")
		os.Exit(64)
	}

	recipient := os.Args[2]
	// be nice and prefix with misfin:// if they forget to put that on
	if recipient[0:9] != "misfin://" {
		recipient = "misfin://" + recipient
	}
	mfre := regexp.MustCompile("^misfin://[^@]+@([^ ]+)$")
	match := mfre.FindStringSubmatch(recipient)
	if match == nil {
		fmt.Fprintln(os.Stderr, "Usage: usercert.pem misfin://[user]@[hostname] < ...")
		os.Exit(65)
	}
	server_addr := match[1] + ":1958"

	header := fmt.Sprintf("%s ", recipient)
	// maximum, minus header, minus trailing \r\n
	remains := 2048 - len(header) - 2
	if remains <= 0 {
		// not judging, but don't want negative remains to read on
		fmt.Fprintln(os.Stderr, "misfin: recipient is too long?")
		os.Exit(1)
	}

	wlim := io.LimitReader(os.Stdin, int64(remains))
	reader := bufio.NewReader(wlim)

	var message string
	for {
		line, err := reader.ReadString('\n')
		if err != nil {
			// KLUGE this tolerate incomplete lines (those
			// lacking the POSIX-mandated ultimate \n)
			if err == io.EOF {
				line = strings.TrimRight(line, "\r\n")
				message += line
				break
			}
			log.Println(err)
			return
		}
		message += line
	}
	// avoid accidental \r\n that would terminate our message early
	// on the server
	message = strings.Replace(message, "\r", "", -1)
	// we'll add this back on in a moment
	message = strings.TrimRight(message, "\n")
	if len(message) == 0 {
		fmt.Fprintln(os.Stderr, "misfin: no message")
		os.Exit(1)
	}
	message += "\r\n"

	client_cert, lerr := tls.LoadX509KeyPair(os.Args[1], os.Args[1])
	if lerr != nil {
		log.Println(lerr)
		return
	}

	ssl_config := &tls.Config{
		// misfin specification calls for a floor of TLS 1.2
		// (section 3, TLS)
		MinVersion:         tls.VersionTLS12,
		Certificates:       []tls.Certificate{client_cert},
		InsecureSkipVerify: true,
	}

	protect.Pledge("inet rpath stdio unveil")
	protect.Unveil("/etc/resolv.conf", "r")
	protect.Unveil("/etc/ssl/cert.pem", "r")
	protect.UnveilBlock()

	conn, err := tls.Dial("tcp", server_addr, ssl_config)
	if err != nil {
		log.Println(err)
		return
	}

	protect.Pledge("stdio")

	// NOTE this used to be two separate Writes but that confused
	// the other implementations, probably because they assumed
	// (incorrectly) that the whole request would be available in a
	// single read
	conn.Write([]byte(header + message))

	// the reference python implementation doesn't work well when
	// this call is made here. so let's not do that...
	//conn.CloseWrite()

	// hopefully the response fits, the cap is mostly for when the
	// server is malicious and tries to stream infinite bytes
	rlim := io.LimitReader(conn, 512)
	resprdr := bufio.NewReader(rlim)
	resp, serr := resprdr.ReadString('\n')
	if serr != nil {
		fmt.Fprintln(os.Stderr, "misfin: warning: read error: %s", serr.Error())
	}
	resp = strings.Replace(resp, "\r", "", -1)
	resp = strings.TrimRight(resp, "\n")
	fmt.Println(resp)
	conn.Close()
}