From b1bf9164a7dc65150ea045906e2e786762585be2 Mon Sep 17 00:00:00 2001 From: David Fifield Date: Sat, 18 Apr 2020 17:47:32 -0600 Subject: [PATCH] -privkey-file and -pubkey-file options. --- dnstt-client/main.go | 37 ++++++++++---- dnstt-server/main.go | 116 +++++++++++++++++++++++++++++++++++++------ noise/noise.go | 48 ++++++++++++++++++ noise/noise_test.go | 37 ++++++++++++++ 4 files changed, 211 insertions(+), 27 deletions(-) create mode 100644 noise/noise_test.go diff --git a/dnstt-client/main.go b/dnstt-client/main.go index ad7e364..bcbf607 100644 --- a/dnstt-client/main.go +++ b/dnstt-client/main.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/base32" "encoding/binary" - "encoding/hex" "flag" "fmt" "io" @@ -152,6 +151,15 @@ func dnsNameCapacity(domain dns.Name) int { return capacity } +func readKeyFromFile(filename string) ([]byte, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + return noise.ReadKey(f) +} + func run(pubkey []byte, domain dns.Name, localAddr *net.TCPAddr, remoteAddr net.Addr, pconn net.PacketConn) error { defer pconn.Close() @@ -228,19 +236,19 @@ func main() { flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), `Usage: - %[1]s [-doh URL|-dot ADDR|-udp ADDR] -pubkey PUBKEY DOMAIN LOCALADDR + %[1]s [-doh URL|-dot ADDR|-udp ADDR] -pubkey-file PUBKEYFILE DOMAIN LOCALADDR Examples: - %[1]s -doh https://resolver.example/dns-query -pubkey 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff t.example.com 127.0.0.1:7000 - %[1]s -dot resolver.example:853 -pubkey 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff t.example.com 127.0.0.1:7000 + %[1]s -doh https://resolver.example/dns-query -pubkey-file server.pub t.example.com 127.0.0.1:7000 + %[1]s -dot resolver.example:853 -pubkey-file server.pub t.example.com 127.0.0.1:7000 `, os.Args[0]) flag.PrintDefaults() } flag.StringVar(&dohURL, "doh", "", "URL of DoH resolver") flag.StringVar(&dotAddr, "dot", "", "address of DoT resolver") - flag.StringVar(&pubkeyString, "pubkey", "", fmt.Sprintf("server public key (%d hex digits)", hex.EncodedLen(noise.KeyLen))) - flag.StringVar(&pubkeyString, "pubkey-file", "", "read server public key from file") + flag.StringVar(&pubkeyString, "pubkey", "", fmt.Sprintf("server public key (%d hex digits)", noise.KeyLen*2)) + flag.StringVar(&pubkeyFilename, "pubkey-file", "", "read server public key from file") flag.StringVar(&udpAddr, "udp", "", "address of UDP DNS resolver") flag.Parse() @@ -262,19 +270,26 @@ Examples: } var pubkey []byte - if pubkeyString != "" { + if pubkeyFilename != "" && pubkeyString != "" { + fmt.Fprintf(os.Stderr, "only one of -pubkey and -pubkey-file may be used\n") + os.Exit(1) + } else if pubkeyFilename != "" { var err error - pubkey, err = hex.DecodeString(pubkeyString) - if err == nil && len(pubkey) != noise.KeyLen { - err = fmt.Errorf("length is %d, expected %d", len(pubkey), noise.KeyLen) + pubkey, err = readKeyFromFile(pubkeyFilename) + if err != nil { + fmt.Fprintf(os.Stderr, "cannot read pubkey from file: %v\n", err) + os.Exit(1) } + } else if pubkeyString != "" { + var err error + pubkey, err = noise.DecodeKey(pubkeyString) if err != nil { fmt.Fprintf(os.Stderr, "pubkey format error: %v\n", err) os.Exit(1) } } if len(pubkey) == 0 { - fmt.Fprintf(os.Stderr, "the -pubkey option is required\n") + fmt.Fprintf(os.Stderr, "the -pubkey or -pubkey-file option is required\n") os.Exit(1) } diff --git a/dnstt-server/main.go b/dnstt-server/main.go index 4d48ff1..9bef120 100644 --- a/dnstt-server/main.go +++ b/dnstt-server/main.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/base32" "encoding/binary" - "encoding/hex" "flag" "fmt" "io" @@ -491,16 +490,85 @@ func sendLoop(dnsConn net.PacketConn, ttConn *turbotunnel.QueuePacketConn, ch <- return nil } -func generateKeypair() error { +func generateKeypair(privkeyFilename, pubkeyFilename string) (err error) { + // Filenames to delete in case of error (avoid leaving partially written + // files). + var toDelete []string + defer func() { + for _, filename := range toDelete { + fmt.Fprintf(os.Stderr, "deleting partially written file %s\n", filename) + if closeErr := os.Remove(filename); closeErr != nil { + fmt.Fprintf(os.Stderr, "cannot remove %s: %v\n", filename, closeErr) + if err == nil { + err = closeErr + } + } + } + }() + privkey, pubkey, err := noise.GenerateKeypair() if err != nil { return err } - fmt.Printf("privkey: %x\n", privkey) - fmt.Printf("pubkey: %x\n", pubkey) + + if privkeyFilename != "" { + // Save the privkey to a file. + f, err := os.Create(privkeyFilename) + if err != nil { + return err + } + toDelete = append(toDelete, privkeyFilename) + err = noise.WriteKey(f, privkey) + if err2 := f.Close(); err == nil { + err = err2 + } + if err != nil { + return err + } + } + + if pubkeyFilename != "" { + // Save the pubkey to a file. + f, err := os.Create(pubkeyFilename) + if err != nil { + return err + } + toDelete = append(toDelete, pubkeyFilename) + err = noise.WriteKey(f, pubkey) + if err2 := f.Close(); err == nil { + err = err2 + } + if err != nil { + return err + } + } + + // All good, allow the written files to remain. + toDelete = nil + + if privkeyFilename != "" { + fmt.Printf("privkey written to %s\n", privkeyFilename) + } else { + fmt.Printf("privkey %x\n", privkey) + } + if pubkeyFilename != "" { + fmt.Printf("pubkey written to %s\n", pubkeyFilename) + } else { + fmt.Printf("pubkey %x\n", pubkey) + } + return nil } +func readKeyFromFile(filename string) ([]byte, error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + defer f.Close() + return noise.ReadKey(f) +} + func run(privkey, pubkey []byte, domain dns.Name, upstream net.Addr, udpAddr string) error { log.Printf("pubkey %x", pubkey) @@ -542,23 +610,27 @@ func run(privkey, pubkey []byte, domain dns.Name, upstream net.Addr, udpAddr str func main() { var genKey bool + var privkeyFilename string var privkeyString string + var pubkeyFilename string var udpAddr string flag.Usage = func() { fmt.Fprintf(flag.CommandLine.Output(), `Usage: - %[1]s -gen-key - %[1]s -udp ADDR -privkey PRIVKEY DOMAIN UPSTREAMADDR + %[1]s -gen-key -privkey-file PRIVKEYFILE -pubkey-file PUBKEYFILE + %[1]s -udp ADDR -privkey-file PRIVKEYFILE DOMAIN UPSTREAMADDR Example: - %[1]s -gen-key - %[1]s -udp 127.0.0.1:5300 -privkey 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef server.key t.example.com 127.0.0.1:8000 + %[1]s -gen-key -privkey-file server.key -pubkey-file server.pub + %[1]s -udp 127.0.0.1:5300 -privkey-file server.key t.example.com 127.0.0.1:8000 `, os.Args[0]) flag.PrintDefaults() } - flag.BoolVar(&genKey, "gen-key", false, "generate a server keypair; print to stdout") - flag.StringVar(&privkeyString, "privkey", "", fmt.Sprintf("server private key (%d hex digits)", hex.EncodedLen(noise.KeyLen))) + flag.BoolVar(&genKey, "gen-key", false, "generate a server keypair; print to stdout or save to files") + flag.StringVar(&privkeyString, "privkey", "", fmt.Sprintf("server private key (%d hex digits)", noise.KeyLen*2)) + flag.StringVar(&privkeyFilename, "privkey-file", "", "read server private key from file (with -gen-key, write to file)") + flag.StringVar(&pubkeyFilename, "pubkey-file", "", "with -gen-key, write server public key to file") flag.StringVar(&udpAddr, "udp", "", "UDP address to listen on (required)") flag.Parse() @@ -570,7 +642,7 @@ Example: flag.Usage() os.Exit(1) } - if err := generateKeypair(); err != nil { + if err := generateKeypair(privkeyFilename, pubkeyFilename); err != nil { fmt.Fprintf(os.Stderr, "cannot generate keypair: %v\n", err) os.Exit(1) } @@ -591,13 +663,25 @@ Example: os.Exit(1) } + if pubkeyFilename != "" { + fmt.Fprintf(os.Stderr, "-pubkey-file may only be used with -gen-key\n") + os.Exit(1) + } + var privkey []byte - if privkeyString != "" { + if privkeyFilename != "" && privkeyString != "" { + fmt.Fprintf(os.Stderr, "only one of -privkey and -privkey-file may be used\n") + os.Exit(1) + } else if privkeyFilename != "" { var err error - privkey, err = hex.DecodeString(privkeyString) - if err == nil && len(privkey) != noise.KeyLen { - err = fmt.Errorf("length is %d, expected %d", len(privkey), noise.KeyLen) + privkey, err = readKeyFromFile(privkeyFilename) + if err != nil { + fmt.Fprintf(os.Stderr, "cannot read privkey from file: %v\n", err) + os.Exit(1) } + } else if privkeyString != "" { + var err error + privkey, err = noise.DecodeKey(privkeyString) if err != nil { fmt.Fprintf(os.Stderr, "privkey format error: %v\n", err) os.Exit(1) @@ -605,7 +689,7 @@ Example: } if len(privkey) == 0 { log.Println("generating a temporary one-time keypair") - log.Println("use the -privkey option for a persistent server keypair") + log.Println("use the -privkey or -privkey-file option for a persistent server keypair") var err error privkey, _, err = noise.GenerateKeypair() if err != nil { diff --git a/noise/noise.go b/noise/noise.go index 5757655..a9622ed 100644 --- a/noise/noise.go +++ b/noise/noise.go @@ -1,12 +1,15 @@ package noise import ( + "bufio" "bytes" "crypto/rand" "encoding/binary" + "encoding/hex" "errors" "fmt" "io" + "strings" "github.com/flynn/noise" ) @@ -205,3 +208,48 @@ func PubkeyFromPrivkey(privkey []byte) []byte { } return pair.Public } + +// ReadKey reads a hex-encoded key from r. r must consist of a single line, with +// or without a '\n' line terminator. The line must consist of KeyLen +// hex-encoded bytes. +func ReadKey(r io.Reader) ([]byte, error) { + br := bufio.NewReader(io.LimitReader(r, 100)) + line, err := br.ReadString('\n') + if err == io.EOF { + err = nil + } + if err == nil { + // Check that we're at EOF. + _, err = br.ReadByte() + if err == io.EOF { + err = nil + } else if err == nil { + err = fmt.Errorf("file contains more than one line") + } + } + if err != nil { + return nil, err + } + line = strings.TrimSuffix(line, "\n") + return DecodeKey(line) +} + +// WriteKey writes the hex-encoded key in a single line to w. +func WriteKey(w io.Writer, key []byte) error { + _, err := fmt.Fprintf(w, "%x\n", key) + return err +} + +// DecodeKey decodes a hex-encoded private or public key. +func DecodeKey(s string) ([]byte, error) { + key, err := hex.DecodeString(s) + if err == nil && len(key) != KeyLen { + err = fmt.Errorf("length is %d, expected %d", len(key), KeyLen) + } + return key, err +} + +// DecodeKey decodes a hex-encoded private or public key. +func EncodeKey(key []byte) string { + return hex.EncodeToString(key) +} diff --git a/noise/noise_test.go b/noise/noise_test.go new file mode 100644 index 0000000..dac100d --- /dev/null +++ b/noise/noise_test.go @@ -0,0 +1,37 @@ +package noise + +import ( + "bytes" + "testing" +) + +func TestReadKey(t *testing.T) { + for _, test := range []struct { + input string + output []byte + }{ + {"", nil}, + {"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde", nil}, + {"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", []byte("\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67\x89\xab\xcd\xef")}, + {"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n", []byte("\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67\x89\xab\xcd\xef")}, + {"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0", nil}, + {"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\nX", nil}, + {"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n\n", nil}, + {"\n0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", nil}, + {"X123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", nil}, + {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", nil}, + } { + output, err := ReadKey(bytes.NewReader([]byte(test.input))) + if test.output == nil { + if err == nil { + t.Errorf("%+q expected error", test.input) + } + } else { + if err != nil { + t.Errorf("%+q returned error %v", test.input, err) + } else if !bytes.Equal(output, test.output) { + t.Errorf("%+q got %x, expected %x", test.input, output, test.output) + } + } + } +}