aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/options/options.go56
-rw-r--r--internal/options/options_test.go64
-rw-r--r--main.go46
3 files changed, 166 insertions, 0 deletions
diff --git a/internal/options/options.go b/internal/options/options.go
new file mode 100644
index 0000000..da63fc2
--- /dev/null
+++ b/internal/options/options.go
@@ -0,0 +1,56 @@
+// Package options provides parsing of user-provided onion-csr options
+package options
+
+import (
+ "encoding/hex"
+ "flag"
+ "fmt"
+ "os"
+)
+
+const usage = `Usage:
+
+ onion-csr -h
+ onion-csr -d HS_DIR -n NONCE
+
+Options:
+
+ -h, --help: Output usage message and exit
+ -d, --hs-dir: Path to hidden service directory
+ -n, --ca-nonce: Nonce provided by a certificate authority in hex
+`
+
+// Options is a collection of onion-csr options
+type Options struct {
+ HSDir string
+ CANonce []byte
+
+ caNonce string
+}
+
+// New parses user-supplied onion-csr options
+func New(cmd string, args []string) (opts Options, err error) {
+ fs := flag.NewFlagSet(cmd, flag.ContinueOnError)
+ fs.Usage = func() { fmt.Fprintf(os.Stderr, "%s", usage) }
+ stringOpt(fs, &opts.HSDir, "hs-dir", "d", "")
+ stringOpt(fs, &opts.caNonce, "ca-nonce", "n", "")
+ if err = fs.Parse(args); err != nil {
+ return opts, err
+ }
+
+ if opts.HSDir == "" {
+ return opts, fmt.Errorf("-d, --hs-dir: must not be an empty string")
+ }
+ if opts.CANonce, err = hex.DecodeString(opts.caNonce); err != nil {
+ return opts, fmt.Errorf("-n, --ca-nonce: %v", err)
+ }
+ if len(opts.CANonce) == 0 {
+ return opts, fmt.Errorf("-n, --ca-nonce: must not be empty string")
+ }
+ return opts, err
+}
+
+func stringOpt(fs *flag.FlagSet, opt *string, short, long, value string) {
+ fs.StringVar(opt, short, value, "")
+ fs.StringVar(opt, long, value, "")
+}
diff --git a/internal/options/options_test.go b/internal/options/options_test.go
new file mode 100644
index 0000000..fda189c
--- /dev/null
+++ b/internal/options/options_test.go
@@ -0,0 +1,64 @@
+package options
+
+import (
+ "fmt"
+ "os"
+ "reflect"
+ "strings"
+ "syscall"
+ "testing"
+)
+
+func TestNew(t *testing.T) {
+ setup(t)
+ defer teardown(t)
+
+ none := Options{}
+ ok := Options{
+ HSDir: "/path/to/hsdir",
+ CANonce: make([]byte, 10),
+ caNonce: "00000000000000000000",
+ }
+ for _, table := range []struct {
+ desc string
+ args string
+ want Options
+ }{
+ {"no options", "", none},
+ {"unknown option --extra", "--extra value", none},
+ {"unknown extra value", "extra", none},
+ {"help option", "-h", none},
+ {"missing --hs-dir", fmt.Sprintf("-n %x", ok.CANonce), none},
+ {"missing --ca-nonce", fmt.Sprintf("-d %s", ok.HSDir), none},
+ {"invalid --ca-nonce", fmt.Sprintf("-d %s -n 0q", ok.HSDir), none},
+ {"valid", fmt.Sprintf("-d %s -n %x", ok.HSDir, ok.CANonce), ok},
+ {"valid", fmt.Sprintf("--hs-dir %s --ca-nonce %x", ok.HSDir, ok.CANonce), ok},
+ } {
+ opts, err := New("onion-csr", strings.Split(table.args, " "))
+ if got, want := err != nil, table.desc != "valid"; got != want {
+ t.Errorf("%s: got error %v but wanted %v: %v", table.desc, got, want, err)
+ }
+ if err != nil {
+ continue
+ }
+ if got, want := opts, table.want; !reflect.DeepEqual(got, want) {
+ t.Errorf("%s: got options\n%v\nbut wanted\n%v\n", table.desc, got, want)
+ }
+ }
+}
+
+func setup(t *testing.T) {
+ t.Helper()
+ var err error
+ if os.Stderr, err = os.CreateTemp("", "onion-csr@rgdd.se"); err != nil {
+ t.Fatalf("create temp file: %v", err)
+ }
+}
+
+func teardown(t *testing.T) {
+ t.Helper()
+ if err := os.Remove(os.Stderr.Name()); err != nil {
+ t.Fatalf("remove temp file: %v", err)
+ }
+ os.Stderr = os.NewFile(uintptr(syscall.Stderr), "/dev/stderr")
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..e6c66ae
--- /dev/null
+++ b/main.go
@@ -0,0 +1,46 @@
+package main
+
+import (
+ "crypto/rand"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+
+ "sauteed-onions.org/onion-csr/internal/options"
+ "sauteed-onions.org/onion-csr/pkg/ocsr"
+ "sauteed-onions.org/onion-csr/pkg/okey"
+)
+
+const entropyBytes = 10 // 80 bits
+
+func main() {
+ opts, err := options.New(os.Args[0], os.Args[1:])
+ if err != nil {
+ if errors.Is(err, flag.ErrHelp) {
+ os.Exit(0)
+ }
+
+ fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
+ os.Exit(1)
+ }
+
+ var applicantNonce [entropyBytes]byte
+ if _, err := io.ReadFull(rand.Reader, applicantNonce[:]); err != nil {
+ fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
+ os.Exit(1)
+ }
+ priv, err := okey.NewFromHSDir(opts.HSDir)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
+ os.Exit(1)
+ }
+ csr, err := ocsr.New(priv, opts.CANonce, applicantNonce[:])
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
+ os.Exit(1)
+ }
+
+ fmt.Fprintf(os.Stdout, "%s", csr)
+}