├── .gitignore ├── Makefile ├── disklog ├── disklog.go └── disklog_test.go ├── LICENSE.md ├── test └── test.go ├── cmds └── dime-a-tap │ └── main.go ├── snoopconn ├── snoopconn.go └── snoopconn_test.go ├── ca ├── generate_test.go ├── store.go ├── generate.go └── store_test.go ├── rwpipe ├── rwpipe.go └── rwpipe_test.go ├── README.md └── server └── server.go /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | /dime-a-tap 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | go build ./cmds/dime-a-tap 3 | 4 | test: 5 | @for i in $$(find . -name '*_test.go' | xargs -n1 dirname | uniq); do \ 6 | go test -timeout=5s $$i || exit 1; \ 7 | done 8 | 9 | clean: 10 | rm -f dime-a-tap 11 | 12 | realclean: clean 13 | go clean -cache 14 | 15 | vet: 16 | go vet --shadow ./... 17 | 18 | fmt: 19 | go fmt ./... 20 | 21 | .PHONY: all read clean test fmt 22 | -------------------------------------------------------------------------------- /disklog/disklog.go: -------------------------------------------------------------------------------- 1 | package disklog 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | ) 8 | 9 | // not actually a packet, just logs distinct chunks of data read/written 10 | func DumpPacket(directory, label, direction string, data []byte) error { 11 | if directory == "" { 12 | return nil 13 | } 14 | filename := fmt.Sprintf("%s/%s.%s.%s", directory, label, time.Now().Format("20060102150405.000000"), direction) 15 | file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0644) 16 | if err != nil { 17 | return fmt.Errorf("unable to log packet: %s", err.Error()) 18 | } 19 | defer file.Close() 20 | file.Write(data) 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Michael Driscoll 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "reflect" 7 | "runtime" 8 | "testing" 9 | ) 10 | 11 | // from https://github.com/benbjohnson/testing (MIT license) 12 | 13 | var CallerDepth = 1 14 | 15 | // assert fails the test if the condition is false. 16 | func Assert(tb testing.TB, condition bool, msg string, v ...interface{}) { 17 | if !condition { 18 | _, file, line, _ := runtime.Caller(CallerDepth) 19 | fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...) 20 | tb.FailNow() 21 | } 22 | } 23 | 24 | // ok fails the test if an err is not nil. 25 | func Ok(tb testing.TB, err error) { 26 | if err != nil { 27 | _, file, line, _ := runtime.Caller(CallerDepth) 28 | fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) 29 | tb.FailNow() 30 | } 31 | } 32 | 33 | // equals fails the test if exp is not equal to act. 34 | func Equals(tb testing.TB, exp, act interface{}) { 35 | if !reflect.DeepEqual(exp, act) { 36 | _, file, line, _ := runtime.Caller(CallerDepth) 37 | fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act) 38 | tb.FailNow() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /cmds/dime-a-tap/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | 9 | "github.com/syncsynchalt/dime-a-tap/ca" 10 | "github.com/syncsynchalt/dime-a-tap/server" 11 | ) 12 | 13 | func dieUsage(err error) { 14 | if err != nil { 15 | fmt.Fprintln(os.Stderr, err) 16 | } 17 | fmt.Fprintf(os.Stderr, "Usage: %s [flags] [port]\n", os.Args[0]) 18 | flag.PrintDefaults() 19 | os.Exit(1) 20 | } 21 | 22 | func main() { 23 | if len(os.Args) == 3 && os.Args[1] == "ca-init" { 24 | err := ca.CreateCAStore(os.Args[2]) 25 | if err != nil { 26 | dieUsage(err) 27 | } else { 28 | fmt.Println("success") 29 | os.Exit(0) 30 | } 31 | } 32 | 33 | caDir := flag.String("cadir", "", "optional path to CA key store (use 'dime-a-tap ca-init {dir}' to create)") 34 | tapPort := flag.Int("tapport", 4430, "localhost port to send unencrypted data over") 35 | rawDir := flag.String("rawdir", "", "optional directory to write raw data written to/from client") 36 | capDir := flag.String("capturedir", "", "optional directory to capture unencrypted data written to/from client") 37 | flag.Parse() 38 | if flag.NArg() != 1 { 39 | dieUsage(fmt.Errorf("No listen port specified")) 40 | } 41 | 42 | port, err := strconv.Atoi(flag.Arg(0)) 43 | if err != nil { 44 | dieUsage(err) 45 | } 46 | 47 | opts := server.Opts{ 48 | Port: port, 49 | RawDir: *rawDir, 50 | CaptureDir: *capDir, 51 | CADir: *caDir, 52 | TapPort: *tapPort, 53 | } 54 | 55 | err = server.Listen(opts) 56 | if err != nil { 57 | panic(err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /disklog/disklog_test.go: -------------------------------------------------------------------------------- 1 | package disklog_test 2 | 3 | import ( 4 | "fmt" 5 | . "github.com/syncsynchalt/dime-a-tap/disklog" 6 | "github.com/syncsynchalt/dime-a-tap/test" 7 | "io/ioutil" 8 | "os" 9 | "strings" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func TestDumpPacketEmptyDir(t *testing.T) { 15 | err := DumpPacket("", "label", "c", []byte("\x01\x02\x03")) 16 | test.Ok(t, err) 17 | } 18 | 19 | func TestDumpPacketNonExistDir(t *testing.T) { 20 | err := DumpPacket("/does/not/exist", "label", "c", []byte("\x01\x02\x03")) 21 | test.Assert(t, err != nil, "error is not set") 22 | prefix := "unable to log packet: open /does/not/exist/label.20" 23 | test.Assert(t, strings.HasPrefix(err.Error(), prefix), 24 | "error [%s] does not have expected prefix [%s]", err, prefix) 25 | } 26 | 27 | func getFirstFile(t *testing.T, dir string) string { 28 | files, err := ioutil.ReadDir(dir) 29 | test.Ok(t, err) 30 | t.Log(files) 31 | return dir + "/" + files[0].Name() 32 | } 33 | 34 | func TestDumpPacket(t *testing.T) { 35 | mydir := fmt.Sprintf("/tmp/golang.test.%d", time.Now().UnixNano()) 36 | os.Mkdir(mydir, 0755) 37 | defer os.RemoveAll(mydir) 38 | err := DumpPacket(mydir, "[::1]:80134", "c", []byte("\x01\x02\x03")) 39 | 40 | file := getFirstFile(t, mydir) 41 | test.Assert(t, strings.HasPrefix(file, mydir+"/[::1]:80134.20"), 42 | "file %s doesn't start with expected prefix", file) 43 | test.Assert(t, strings.HasSuffix(file, ".c"), 44 | "file %s doesn't end with expected suffix", file) 45 | 46 | data, err := ioutil.ReadFile(file) 47 | test.Ok(t, err) 48 | test.Equals(t, []byte("\x01\x02\x03"), data) 49 | } 50 | -------------------------------------------------------------------------------- /snoopconn/snoopconn.go: -------------------------------------------------------------------------------- 1 | package snoopconn 2 | 3 | // intercepts the net.Conn interface and records the first 10kiB of data. 4 | // Optionally writes all reads/writes to RawDir 5 | 6 | import ( 7 | "log" 8 | "net" 9 | "os" 10 | 11 | "github.com/syncsynchalt/dime-a-tap/disklog" 12 | ) 13 | 14 | const SnoopBytes = 10240 15 | 16 | type TattleConn struct { 17 | net.Conn 18 | rawDir string 19 | log *log.Logger 20 | remoteName string 21 | ReadData []byte 22 | WriteData []byte 23 | } 24 | 25 | func New(conn net.Conn, rawDir string) *TattleConn { 26 | return &TattleConn{ 27 | Conn: conn, 28 | rawDir: rawDir, 29 | log: log.New(os.Stdout, conn.RemoteAddr().String()+" ", log.Ldate|log.Ltime), 30 | remoteName: conn.RemoteAddr().String(), 31 | ReadData: make([]byte, 0, SnoopBytes), 32 | WriteData: make([]byte, 0, SnoopBytes), 33 | } 34 | } 35 | 36 | func (c *TattleConn) Read(b []byte) (n int, err error) { 37 | n, err = c.Conn.Read(b) 38 | if err != nil { 39 | return n, err 40 | } 41 | 42 | err = disklog.DumpPacket(c.rawDir, c.remoteName, "c", b[:n]) 43 | if err != nil { 44 | log.Println("unable to dump raw:", err) 45 | // ignore error 46 | } 47 | 48 | if len(c.ReadData) < SnoopBytes { 49 | na := n 50 | if len(c.ReadData)+na > SnoopBytes { 51 | na = SnoopBytes - len(c.ReadData) 52 | } 53 | c.ReadData = append(c.ReadData, b[:na]...) 54 | } 55 | return n, nil 56 | } 57 | 58 | func (c *TattleConn) Write(b []byte) (n int, err error) { 59 | n, err = c.Conn.Write(b) 60 | if err != nil { 61 | return n, err 62 | } 63 | 64 | err = disklog.DumpPacket(c.rawDir, c.remoteName, "s", b[:n]) 65 | if err != nil { 66 | log.Println("unable to dump raw:", err) 67 | // ignore error 68 | } 69 | 70 | if len(c.WriteData) < 10240 { 71 | na := n 72 | if len(c.WriteData)+na > SnoopBytes { 73 | na = SnoopBytes - len(c.WriteData) 74 | } 75 | c.WriteData = append(c.WriteData, b[:na]...) 76 | } 77 | return n, nil 78 | } 79 | -------------------------------------------------------------------------------- /ca/generate_test.go: -------------------------------------------------------------------------------- 1 | package ca_test 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | "github.com/syncsynchalt/dime-a-tap/ca" 12 | "github.com/syncsynchalt/dime-a-tap/test" 13 | ) 14 | 15 | func TestGenKey(t *testing.T) { 16 | key, err := ca.GenerateCAKey() 17 | test.Ok(t, err) 18 | t.Log(key) 19 | prefix := "-----BEGIN RSA PRIVATE KEY-----\nM" 20 | test.Equals(t, prefix, string(key[:len(prefix)])) 21 | // fmt.Print(key) 22 | } 23 | 24 | func TestGenCert(t *testing.T) { 25 | key, err := ca.GenerateCAKey() 26 | test.Ok(t, err) 27 | cert, err := ca.GenerateCACert(key) 28 | test.Ok(t, err) 29 | prefix := "-----BEGIN CERTIFICATE-----\nM" 30 | test.Equals(t, prefix, string(cert[:len(prefix)])) 31 | // fmt.Print(cert) 32 | } 33 | 34 | func TestGenCa(t *testing.T) { 35 | mydir := fmt.Sprintf("/tmp/golang.test.%d", time.Now().UnixNano()) 36 | defer os.RemoveAll(mydir) 37 | err := ca.CreateCAStore(mydir) 38 | test.Ok(t, err) 39 | 40 | certdata, err := ioutil.ReadFile(mydir + "/ca.crt") 41 | test.Ok(t, err) 42 | prefix := "-----BEGIN CERTIFICATE-----\nM" 43 | test.Equals(t, prefix, string(certdata)[:len(prefix)]) 44 | 45 | keydata, err := ioutil.ReadFile(mydir + "/ca.key") 46 | test.Ok(t, err) 47 | prefix = "-----BEGIN RSA PRIVATE KEY-----\nM" 48 | test.Equals(t, prefix, string(keydata)[:len(prefix)]) 49 | } 50 | 51 | func TestGenCaNotDir(t *testing.T) { 52 | mydir := fmt.Sprintf("/tmp/golang.test.%d", time.Now().UnixNano()) 53 | defer os.RemoveAll(mydir) 54 | ioutil.WriteFile(mydir, []byte("abc\n"), 0644) 55 | 56 | err := ca.CreateCAStore(mydir) 57 | test.Assert(t, strings.HasSuffix(err.Error(), "not a directory"), "error [%s] not expected format", err) 58 | } 59 | 60 | func TestGenCaNoTwice(t *testing.T) { 61 | mydir := fmt.Sprintf("/tmp/golang.test.%d", time.Now().UnixNano()) 62 | defer os.RemoveAll(mydir) 63 | 64 | err := ca.CreateCAStore(mydir) 65 | test.Ok(t, err) 66 | err = ca.CreateCAStore(mydir) 67 | test.Assert(t, strings.HasSuffix(err.Error(), "file exists"), "error [%s] not expected format", err) 68 | } 69 | -------------------------------------------------------------------------------- /rwpipe/rwpipe.go: -------------------------------------------------------------------------------- 1 | package rwpipe 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "os" 8 | 9 | "github.com/syncsynchalt/dime-a-tap/disklog" 10 | ) 11 | 12 | // pipe the inputs and outputs of each conn to each other 13 | func PipeConns(conn1 net.Conn, name1 string, conn2 net.Conn, name2 string, captureDir string) error { 14 | logName := conn1.RemoteAddr().String() 15 | l := log.New(os.Stdout, logName+" ", log.Ldate|log.Ltime) 16 | 17 | readFrom1 := make(chan []byte) 18 | go func() { 19 | // reads from conn1 and writes to readFrom1 20 | b := make([]byte, 4096) 21 | for { 22 | n, err := conn1.Read(b) 23 | if n != 0 { 24 | bcopy := make([]byte, n) 25 | copy(bcopy, b[:n]) 26 | readFrom1 <- bcopy 27 | } 28 | if err != nil { 29 | break 30 | } 31 | } 32 | close(readFrom1) 33 | }() 34 | 35 | readFrom2 := make(chan []byte) 36 | go func() { 37 | // reads from conn2 and writes to readFrom2 38 | b := make([]byte, 4096) 39 | for { 40 | n, err := conn2.Read(b) 41 | if n != 0 { 42 | bcopy := make([]byte, n) 43 | copy(bcopy, b[:n]) 44 | readFrom2 <- bcopy 45 | } 46 | if err != nil { 47 | break 48 | } 49 | } 50 | close(readFrom2) 51 | }() 52 | 53 | loop: 54 | for { 55 | select { 56 | case b, more := <-readFrom1: 57 | err := disklog.DumpPacket(captureDir, logName, "c", b) 58 | if err != nil { 59 | l.Println("unable to dump clean:", err) 60 | // ignore error 61 | } 62 | 63 | for len(b) > 0 { 64 | n, err := conn2.Write(b) 65 | if err != nil { 66 | return fmt.Errorf("unable to write data to %s: %s", name2, err) 67 | } 68 | b = b[n:] 69 | } 70 | 71 | if !more { 72 | l.Printf("%s conn closed\n", name1) 73 | break loop 74 | } 75 | case b, more := <-readFrom2: 76 | err := disklog.DumpPacket(captureDir, logName, "s", b) 77 | if err != nil { 78 | l.Println("unable to dump clean:", err) 79 | // ignore error 80 | } 81 | 82 | for len(b) > 0 { 83 | n, err := conn1.Write(b) 84 | if err != nil { 85 | return fmt.Errorf("unable to write data to %s: %s", name1, err) 86 | } 87 | b = b[n:] 88 | } 89 | if !more { 90 | l.Printf("%s conn closed\n", name2) 91 | break loop 92 | } 93 | } 94 | } 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dime-a-tap 2 | 3 | MITM proxy to make TLS/SSL traffic readable in the clear. 4 | 5 | Unencrypted traffic is sent over loopback to make it easily tcpdumpable. 6 | 7 | ### Getting started 8 | 9 | To start the proxy on port 443 (HTTPS): 10 | 11 | ``` 12 | $ go install github.com/syncsynchalt/dime-a-tap/cmds/dime-a-tap@latest 13 | $ export PATH=$PATH:~/go/bin 14 | $ dime-a-tap 443 15 | ``` 16 | 17 | Use /etc/hosts, captive DNS, or similar to redirect hosts and devices to your proxy for a given hostname. 18 | 19 | ### Creating a certificate store 20 | 21 | To avoid security warnings you'll want to create a CA and distribute it to your devices: 22 | 23 | ``` 24 | $ dime-a-tap ca-init /tmp/cadir 25 | $ dime-a-tap -cadir /tmp/cadir 443 26 | ``` 27 | 28 | Install the certificate in `/tmp/cadir/ca.crt` as a trusted CA on your hosts or devices. 29 | 30 | ### Capturing the unencrypted data 31 | 32 | To capture intercepted data, there are two options. 33 | 34 | Use `-capturedir {dir}` to write the unencrypted client (.c) and server (.s) conversation to files in that dir. Example: 35 | ``` 36 | $ mkdir /tmp/captures 37 | $ dime-a-tap -capturedir /tmp/captures 443 & 38 | (send traffic through the tap) 39 | $ ls /tmp/captures 40 | total 56 41 | -rw-r--r-- 1 user wheel 75 Oct 4 12:45 192.168.69.42:52981.20181004124516.667781.c 42 | -rw-r--r-- 1 user wheel 756 Oct 4 12:45 192.168.69.42:52981.20181004124516.733675.s 43 | -rw-r--r-- 1 user wheel 0 Oct 4 12:45 192.168.69.42:52981.20181004124516.735306.c 44 | -rw-r--r-- 1 user wheel 75 Oct 4 12:45 192.168.69.42:52989.20181004124551.808247.c 45 | -rw-r--r-- 1 user wheel 756 Oct 4 12:45 192.168.69.42:52989.20181004124551.875861.s 46 | -rw-r--r-- 1 user wheel 0 Oct 4 12:45 192.168.69.42:52989.20181004124551.877488.c 47 | -rw-r--r-- 1 user wheel 75 Oct 4 12:46 192.168.69.42:52992.20181004124609.494528.c 48 | -rw-r--r-- 1 user wheel 297 Oct 4 12:46 192.168.69.42:52992.20181004124609.554621.s 49 | -rw-r--r-- 1 user wheel 459 Oct 4 12:46 192.168.69.42:52992.20181004124609.555327.s 50 | -rw-r--r-- 1 user wheel 0 Oct 4 12:46 192.168.69.42:52992.20181004124609.556733.c 51 | ``` 52 | 53 | Or use `tcpdump` on localhost:4430 to create a pcap file suitable for use with wireshark. Example: 54 | ``` 55 | $ dime-a-tap 443 & 56 | $ tcpdump -i lo0 -s 0 -w capture.pcap port 4430 57 | (send traffic through the tap) 58 | ``` 59 | -------------------------------------------------------------------------------- /rwpipe/rwpipe_test.go: -------------------------------------------------------------------------------- 1 | package rwpipe_test 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/syncsynchalt/dime-a-tap/rwpipe" 11 | "github.com/syncsynchalt/dime-a-tap/server" 12 | "github.com/syncsynchalt/dime-a-tap/test" 13 | ) 14 | 15 | func makeConnPair(t *testing.T, port int) (net.Conn, net.Conn) { 16 | l, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) 17 | test.Ok(t, err) 18 | 19 | c1 := make(chan net.Conn) 20 | go func() { 21 | conn1, err := l.Accept() 22 | test.Ok(t, err) 23 | c1 <- conn1 24 | }() 25 | c2 := make(chan net.Conn) 26 | go func() { 27 | conn2, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) 28 | test.Ok(t, err) 29 | c2 <- conn2 30 | }() 31 | conn1 := <-c1 32 | conn2 := <-c2 33 | l.Close() 34 | return conn1, conn2 35 | } 36 | 37 | func TestRWPipe(t *testing.T) { 38 | 39 | // conn1 <-tcp-> conn2 <-rwpipe-> conn3 <-tcp-> conn4 40 | 41 | conn1, conn2, err := server.MakeConnPair(8314) 42 | test.Ok(t, err) 43 | conn3, conn4, err := server.MakeConnPair(8315) 44 | test.Ok(t, err) 45 | 46 | // the data we'll send on each end 47 | data1 := strings.Split("asdfasdf\nasfasdfasdf\nasdfasdfASdfasdf\nasdfasdfasDFasdf\nasdfasDFasdfasdf\n", "\n") 48 | data4 := strings.Split("iuewriouy\nwieoruower\nnaseroiuw\nwerqweroiqweroiwqeiuor\noweuruoweiruwer\n", "\n") 49 | 50 | // join conn2 to conn3 51 | done := make(chan bool) 52 | go func() { 53 | rwpipe.PipeConns(conn2, "conn2", conn3, "conn3", "") 54 | conn2.Close() 55 | conn3.Close() 56 | done <- true 57 | }() 58 | 59 | // alternate write and read on conn1 60 | chan1 := make(chan string) 61 | go func() { 62 | defer conn1.Close() 63 | from1 := make([]byte, 0) 64 | scan1 := bufio.NewScanner(conn1) 65 | for _, s := range data1 { 66 | _, err := conn1.Write([]byte(s + "\n")) 67 | test.Ok(t, err) 68 | scanned := scan1.Scan() 69 | test.Assert(t, scanned, "conn1 didn't scan") 70 | test.Ok(t, scan1.Err()) 71 | from1 = append(from1, scan1.Bytes()...) 72 | from1 = append(from1, "\n"...) 73 | } 74 | chan1 <- string(from1) 75 | }() 76 | 77 | // alternate write and read on conn4 78 | chan4 := make(chan string) 79 | go func() { 80 | defer conn4.Close() 81 | from4 := make([]byte, 0) 82 | scan4 := bufio.NewScanner(conn4) 83 | for _, s := range data4 { 84 | _, err := conn4.Write([]byte(s + "\n")) 85 | test.Ok(t, err) 86 | scanned := scan4.Scan() 87 | test.Assert(t, scanned, "conn4 didn't scan") 88 | test.Ok(t, scan4.Err()) 89 | from4 = append(from4, scan4.Bytes()...) 90 | from4 = append(from4, "\n"...) 91 | } 92 | chan4 <- string(from4) 93 | }() 94 | 95 | read1 := <-chan1 96 | read4 := <-chan4 97 | _ = <-done 98 | 99 | test.Equals(t, strings.Join(data4, "\n")+"\n", read1) 100 | test.Equals(t, strings.Join(data1, "\n")+"\n", read4) 101 | } 102 | -------------------------------------------------------------------------------- /snoopconn/snoopconn_test.go: -------------------------------------------------------------------------------- 1 | package snoopconn_test 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/syncsynchalt/dime-a-tap/snoopconn" 10 | "github.com/syncsynchalt/dime-a-tap/test" 11 | ) 12 | 13 | func TestTattleConn(t *testing.T) { 14 | l, err := net.Listen("tcp", ":5436") 15 | test.Ok(t, err) 16 | defer l.Close() 17 | 18 | go func() { 19 | conn, err := l.Accept() 20 | if err != nil { 21 | t.Log("gofunc error accepting:", err) 22 | } 23 | defer conn.Close() 24 | b := make([]byte, 1000) 25 | _, err = conn.Read(b) 26 | if err != nil { 27 | t.Log("gofunc error reading:", err) 28 | } 29 | _, err = conn.Write([]byte("RESPONSE\r\n")) 30 | if err != nil { 31 | t.Log("gofunc error writing:", err) 32 | } 33 | }() 34 | 35 | conn, err := net.Dial("tcp", ":5436") 36 | test.Ok(t, err) 37 | tc := snoopconn.New(conn, "") 38 | defer tc.Close() 39 | n, err := tc.Write([]byte("QUERY\r\n")) 40 | test.Ok(t, err) 41 | test.Equals(t, 7, n) 42 | 43 | b := make([]byte, 1000) 44 | n, err = tc.Read(b) 45 | test.Ok(t, err) 46 | test.Equals(t, 10, n) 47 | test.Equals(t, []byte("RESPONSE\r\n"), b[:n]) 48 | test.Equals(t, []byte("QUERY\r\n"), tc.WriteData) 49 | test.Equals(t, []byte("RESPONSE\r\n"), tc.ReadData) 50 | } 51 | 52 | func TestTattleConnMulti(t *testing.T) { 53 | l, err := net.Listen("tcp", ":5436") 54 | test.Ok(t, err) 55 | defer l.Close() 56 | const testSize = 100000 57 | 58 | go func() { 59 | conn, err := l.Accept() 60 | if err != nil { 61 | t.Log("gofunc error accepting:", err) 62 | } 63 | defer conn.Close() 64 | bb := make([]byte, 805) 65 | i := 0 66 | for i < testSize { 67 | n1, err := conn.Read(bb) 68 | if err != nil { 69 | t.Log("Stopping reads from socket at i=", i, err) 70 | break 71 | } 72 | n2, err := conn.Write([]byte(strings.Repeat("y", n1))) 73 | if err != nil { 74 | t.Log("Stopping writes from socket at i=", i, err) 75 | break 76 | } 77 | if n1 != n2 { 78 | t.Log("Mismatched reads and writes at i=", i) 79 | break 80 | } 81 | i += n1 82 | } 83 | }() 84 | 85 | conn, err := net.Dial("tcp", ":5436") 86 | test.Ok(t, err) 87 | tc := snoopconn.New(conn, "") 88 | defer tc.Close() 89 | 90 | send := []byte(strings.Repeat("x", testSize)) 91 | receive := []byte("") 92 | 93 | b := make([]byte, 1024) 94 | for { 95 | toSend := send[:] 96 | if len(toSend) > 2048 { 97 | toSend = toSend[:2048] 98 | } 99 | n, err := tc.Write(toSend) 100 | test.Ok(t, err) 101 | // t.Logf("Sent %d bytes", n) 102 | send = send[n:] 103 | 104 | n, err = tc.Read(b) 105 | if err == io.EOF { 106 | break 107 | } 108 | test.Ok(t, err) 109 | // t.Logf("Read %d bytes", n) 110 | receive = append(receive, b[:n]...) 111 | } 112 | 113 | test.Equals(t, testSize, len(receive)) 114 | test.Equals(t, []byte(strings.Repeat("y", testSize)), receive) 115 | 116 | test.Equals(t, 10240, len(tc.WriteData)) 117 | test.Equals(t, []byte(strings.Repeat("x", 10240)), tc.WriteData) 118 | test.Equals(t, 10240, len(tc.ReadData)) 119 | test.Equals(t, []byte(strings.Repeat("y", 10240)), tc.ReadData) 120 | } 121 | -------------------------------------------------------------------------------- /ca/store.go: -------------------------------------------------------------------------------- 1 | package ca 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "regexp" 8 | "strings" 9 | "sync" 10 | ) 11 | 12 | type Store struct { 13 | lock sync.Mutex 14 | keys map[string][]byte 15 | certs map[string][]byte 16 | dir string 17 | cakey []byte 18 | cacert []byte 19 | } 20 | 21 | func NewStore(directory string) (*Store, error) { 22 | var cakey, cacert []byte 23 | var err1, err2 error 24 | if directory == "" { 25 | cakey, err1 = GenerateCAKey() 26 | cacert, err2 = GenerateCACert(cakey) 27 | } else { 28 | cakey, err1 = ioutil.ReadFile(directory + "/ca.key") 29 | cacert, err2 = ioutil.ReadFile(directory + "/ca.crt") 30 | } 31 | if err1 != nil { 32 | return nil, err1 33 | } 34 | if err2 != nil { 35 | return nil, err2 36 | } 37 | return &Store{ 38 | keys: make(map[string][]byte), 39 | certs: make(map[string][]byte), 40 | dir: directory, 41 | cakey: cakey, 42 | cacert: cacert, 43 | }, nil 44 | } 45 | 46 | func (store *Store) keyfile(domain string) string { 47 | if store.dir != "" { 48 | return fmt.Sprintf("%s/domain-%s.key", store.dir, domain) 49 | } else { 50 | return "" 51 | } 52 | } 53 | 54 | func (store *Store) certfile(domain string) string { 55 | if store.dir != "" { 56 | return fmt.Sprintf("%s/domain-%s.crt", store.dir, domain) 57 | } else { 58 | return "" 59 | } 60 | } 61 | 62 | func (store *Store) writeFile(filename string, data []byte, perm os.FileMode) error { 63 | if store.dir != "" { 64 | return writeFileExcl(filename, data, perm) 65 | } else { 66 | return nil 67 | } 68 | } 69 | 70 | func (store *Store) exists(filename string) bool { 71 | _, err := os.Stat(filename) 72 | return err == nil 73 | } 74 | 75 | func (store *Store) GetCertificate(domain string) (key, cert []byte, err error) { 76 | domain = strings.ToLower(domain) 77 | if !isSafeDomain(domain) { 78 | return nil, nil, fmt.Errorf("domain %s is not valid", domain) 79 | } 80 | 81 | store.lock.Lock() 82 | defer store.lock.Unlock() 83 | 84 | if store.keys[domain] == nil { 85 | filename := store.keyfile(domain) 86 | if store.exists(filename) { 87 | store.keys[domain], err = ioutil.ReadFile(filename) 88 | if err != nil { 89 | return nil, nil, err 90 | } 91 | } else { 92 | store.keys[domain], err = generateKey() 93 | if err != nil { 94 | return nil, nil, err 95 | } 96 | err = store.writeFile(filename, store.keys[domain], 0600) 97 | if err != nil { 98 | return nil, nil, err 99 | } 100 | } 101 | } 102 | 103 | if store.certs[domain] == nil { 104 | filename := store.certfile(domain) 105 | if store.exists(filename) { 106 | store.certs[domain], err = ioutil.ReadFile(filename) 107 | if err != nil { 108 | return nil, nil, err 109 | } 110 | } else { 111 | store.certs[domain], err = generateCert(domain, store.keys[domain], store.cakey, store.cacert) 112 | if err != nil { 113 | return nil, nil, err 114 | } 115 | err = store.writeFile(filename, store.certs[domain], 0644) 116 | if err != nil { 117 | return nil, nil, err 118 | } 119 | } 120 | } 121 | 122 | return store.keys[domain], store.certs[domain], nil 123 | } 124 | 125 | func isSafeDomain(domain string) bool { 126 | if len(domain) == 0 { 127 | return false 128 | } 129 | if domain[0] == '.' { 130 | return false 131 | } 132 | return regexp.MustCompile(`^[-\.a-z0-9]+$`).MatchString(domain) 133 | } 134 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "log" 7 | "net" 8 | "os" 9 | "strconv" 10 | 11 | "github.com/syncsynchalt/dime-a-tap/ca" 12 | "github.com/syncsynchalt/dime-a-tap/rwpipe" 13 | "github.com/syncsynchalt/dime-a-tap/snoopconn" 14 | ) 15 | 16 | type Opts struct { 17 | // port to listen on 18 | Port int 19 | // optional dir to store raw read/write info 20 | RawDir string 21 | // optional dir to store unencrypted read/write info 22 | CaptureDir string 23 | // if not set then creates memory-only version 24 | CADir string 25 | // unencrypted data will be sent over localhost:4430 for tcpdump-ability 26 | TapPort int 27 | } 28 | 29 | // intercepts Accept() and wraps Conn in a SnoopConn 30 | type ListenWrap struct { 31 | net.Listener 32 | opts Opts 33 | } 34 | 35 | // override of net.Listener.Accept() 36 | func (l *ListenWrap) Accept() (net.Conn, error) { 37 | conn, err := l.Listener.Accept() 38 | if err != nil { 39 | return nil, err 40 | } 41 | tc := snoopconn.New(conn, l.opts.RawDir) 42 | return tc, err 43 | } 44 | 45 | func Listen(opts Opts) error { 46 | l := log.New(os.Stdout, "", log.Ldate|log.Ltime) 47 | 48 | caStore, err := ca.NewStore(opts.CADir) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | ln, err := net.Listen("tcp", ":"+strconv.Itoa(opts.Port)) 54 | if err != nil { 55 | return err 56 | } 57 | snooplisten := ListenWrap{Listener: ln, opts: opts} 58 | l.Printf("started listen on port %d\n", opts.Port) 59 | tlslisten := tls.NewListener(&snooplisten, &tls.Config{ 60 | GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { 61 | return getCertificate(hello, l, caStore) 62 | }, 63 | }) 64 | 65 | for { 66 | conn, err := tlslisten.Accept() 67 | if err != nil { 68 | l.Panicln("unable to accept connection:", err) 69 | } 70 | go handleConnection(conn, &opts) 71 | } 72 | } 73 | 74 | func getSNIServerName(conn net.Conn) (string, error) { 75 | tlsConn, ok := conn.(*tls.Conn) 76 | if !ok { 77 | return "", fmt.Errorf("unable to convert connection to tls.Conn") 78 | } 79 | // needed to set up ServerName 80 | err := tlsConn.Handshake() 81 | if err != nil { 82 | return "", fmt.Errorf("error performing handshake: %s", err) 83 | } 84 | serverName := tlsConn.ConnectionState().ServerName 85 | if serverName == "" { 86 | return "", fmt.Errorf("client did not send hostname (SNI), unable to proceed") 87 | } 88 | return serverName, nil 89 | } 90 | 91 | func handleConnection(clientConn net.Conn, opts *Opts) (err error) { 92 | defer clientConn.Close() 93 | 94 | // create and connect the following: 95 | // server <-tcp-> serverConn <-rwpipe-> tapPort <-tcp-> tapAnon <-rwpipe-> clientConn <-tcp-> client 96 | // tapPort: localhost:4430 97 | // tapAnon: localhost:{ephemeral} 98 | 99 | clientName := clientConn.RemoteAddr().String() 100 | l := log.New(os.Stdout, clientName+" ", log.Ldate|log.Ltime) 101 | defer func() { 102 | if err != nil { 103 | l.Println(err) 104 | } 105 | }() 106 | 107 | serverName, err := getSNIServerName(clientConn) 108 | if err != nil { 109 | return err 110 | } 111 | l.Printf("intercepted connection to %s:%d\n", serverName, opts.Port) 112 | 113 | serverConn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", serverName, opts.Port), &tls.Config{ 114 | ServerName: serverName, 115 | }) 116 | if err != nil { 117 | return fmt.Errorf("unable to connect to %s: %s\n", serverName, err) 118 | } 119 | defer serverConn.Close() 120 | l.Printf("connected to %s:%d\n", serverName, opts.Port) 121 | 122 | tapPort, tapAnon, err := MakeConnPair(opts.TapPort) 123 | if err != nil { 124 | return err 125 | } 126 | defer tapPort.Close() 127 | defer tapAnon.Close() 128 | 129 | go func() { 130 | err = rwpipe.PipeConns(serverConn, "server", tapPort, "tap", "") 131 | if err != nil { 132 | l.Println(err) 133 | } 134 | }() 135 | return rwpipe.PipeConns(clientConn, "client", tapAnon, "tapanon", opts.CaptureDir) 136 | } 137 | 138 | func getCertificate(hello *tls.ClientHelloInfo, l *log.Logger, caStore *ca.Store) (*tls.Certificate, error) { 139 | if hello.ServerName == "" { 140 | return nil, fmt.Errorf("server did not provide hostname in SNI") 141 | } 142 | l.Printf("returning certificate for %s\n", hello.ServerName) 143 | 144 | key, cert, err := caStore.GetCertificate(hello.ServerName) 145 | kp, err := tls.X509KeyPair(cert, key) 146 | return &kp, err 147 | } 148 | 149 | // given a port, creates a pair of connections in the form of 150 | // localhost:port and localhost:ephemeralport that are connected 151 | // to each other 152 | func MakeConnPair(port int) (onPort, anonPort net.Conn, err error) { 153 | l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)) 154 | if err != nil { 155 | return nil, nil, err 156 | } 157 | defer l.Close() 158 | 159 | type connAndError struct { 160 | conn net.Conn 161 | err error 162 | } 163 | 164 | c1 := make(chan *connAndError) 165 | go func() { 166 | conn1, err := l.Accept() 167 | c1 <- &connAndError{conn1, err} 168 | }() 169 | c2 := make(chan *connAndError) 170 | go func() { 171 | conn2, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) 172 | c2 <- &connAndError{conn2, err} 173 | }() 174 | 175 | ce1 := <-c1 176 | ce2 := <-c2 177 | if ce1.err != nil || ce2.err != nil { 178 | if ce1.conn != nil { 179 | ce1.conn.Close() 180 | } 181 | if ce2.conn != nil { 182 | ce2.conn.Close() 183 | } 184 | if ce1.err != nil { 185 | return nil, nil, ce1.err 186 | } else { 187 | return nil, nil, ce2.err 188 | } 189 | } 190 | return ce1.conn, ce2.conn, nil 191 | } 192 | -------------------------------------------------------------------------------- /ca/generate.go: -------------------------------------------------------------------------------- 1 | package ca 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/sha1" 7 | "crypto/x509" 8 | "crypto/x509/pkix" 9 | "encoding/asn1" 10 | "encoding/pem" 11 | "fmt" 12 | "math/big" 13 | "os" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | const rsaBits = 2048 19 | 20 | // generate a RSA key and return it as a PEM block 21 | func generateKey() ([]byte, error) { 22 | key, err := rsa.GenerateKey(rand.Reader, rsaBits) 23 | if err != nil { 24 | return nil, err 25 | } 26 | derKey := x509.MarshalPKCS1PrivateKey(key) 27 | return derToPem(derKey, "RSA PRIVATE KEY") 28 | } 29 | 30 | // generate a leaf certificate for domain 31 | func generateCert(domain string, domainKeyPEM, caKeyPEM, caCertPEM []byte) ([]byte, error) { 32 | domainKey, err := pemToRSA(domainKeyPEM) 33 | if err != nil { 34 | return nil, err 35 | } 36 | caKey, err := pemToRSA(caKeyPEM) 37 | if err != nil { 38 | return nil, err 39 | } 40 | caCert, err := pemToCert(caCertPEM) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | template := x509.Certificate{ 46 | SerialNumber: big.NewInt(time.Now().UnixNano()), 47 | Subject: pkix.Name{ 48 | Country: []string{"US"}, 49 | Organization: []string{"Dime-A-Tap"}, 50 | CommonName: domain, 51 | }, 52 | NotBefore: time.Now().Add(-600).UTC(), 53 | NotAfter: time.Now().AddDate(1, 0, 0).UTC(), 54 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, 55 | ExtKeyUsage: []x509.ExtKeyUsage{ 56 | x509.ExtKeyUsageClientAuth, 57 | x509.ExtKeyUsageServerAuth, 58 | }, 59 | } 60 | 61 | derCert, err := x509.CreateCertificate(rand.Reader, &template, caCert, domainKey.Public(), caKey) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | return derToPem(derCert, "CERTIFICATE") 67 | } 68 | 69 | func GenerateCAKey() ([]byte, error) { 70 | return generateKey() 71 | } 72 | 73 | // generate a self-signed certificate for key suitable for CA use. 74 | // If these defaults aren't suitable, build your own using openssl or similar. 75 | func GenerateCACert(pemKey []byte) ([]byte, error) { 76 | derKey, _ := pem.Decode([]byte(pemKey)) 77 | if derKey == nil { 78 | return nil, fmt.Errorf("unable to decode private key in PEM format: %s", pemKey) 79 | } 80 | rsaKey, err := x509.ParsePKCS1PrivateKey(derKey.Bytes) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | template := x509.Certificate{ 86 | SerialNumber: big.NewInt(1), 87 | Subject: pkix.Name{ 88 | Country: []string{"US"}, 89 | Organization: []string{"Dime-A-Tap"}, 90 | OrganizationalUnit: []string{"Fake CA"}, 91 | }, 92 | NotBefore: time.Now().Add(-3600).UTC(), 93 | NotAfter: time.Now().AddDate(10, 0, 0).UTC(), 94 | KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, 95 | BasicConstraintsValid: true, 96 | IsCA: true, 97 | } 98 | template.SubjectKeyId, err = generateSubjectKeyId(rsaKey) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | derCert, err := x509.CreateCertificate(rand.Reader, &template, &template, rsaKey.Public(), rsaKey) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | return derToPem(derCert, "CERTIFICATE") 109 | } 110 | 111 | func derToPem(bytes []byte, pemType string) ([]byte, error) { 112 | sout := strings.Builder{} 113 | err := pem.Encode(&sout, &pem.Block{Type: pemType, Bytes: bytes}) 114 | return []byte(sout.String()), err 115 | } 116 | 117 | func pemToRSA(pemkey []byte) (*rsa.PrivateKey, error) { 118 | derKey, _ := pem.Decode(pemkey) 119 | if derKey == nil { 120 | return nil, fmt.Errorf("unable to decode private key in PEM format") 121 | } 122 | if derKey.Type != "RSA PRIVATE KEY" { 123 | return nil, fmt.Errorf("unexpected private key type %s", derKey.Type) 124 | } 125 | return x509.ParsePKCS1PrivateKey(derKey.Bytes) 126 | } 127 | 128 | func pemToCert(pemcert []byte) (*x509.Certificate, error) { 129 | derCert, _ := pem.Decode(pemcert) 130 | if derCert == nil { 131 | return nil, fmt.Errorf("unable to decode certificate in PEM format") 132 | } 133 | if derCert.Type != "CERTIFICATE" { 134 | return nil, fmt.Errorf("unexpected certificate type %s", derCert.Type) 135 | } 136 | return x509.ParseCertificate(derCert.Bytes) 137 | } 138 | 139 | // required for CA certs, we generate ours by SHA1(pubkey) 140 | func generateSubjectKeyId(key *rsa.PrivateKey) ([]byte, error) { 141 | // hash the public key for the subjectKeyId 142 | pubKeyOnly := rsa.PublicKey{N: key.PublicKey.N, E: key.PublicKey.E} 143 | bytes, err := asn1.Marshal(pubKeyOnly) 144 | if err != nil { 145 | return nil, err 146 | } 147 | hash := sha1.Sum(bytes) 148 | return hash[:], nil 149 | } 150 | 151 | func CreateCAStore(directory string) error { 152 | err := os.MkdirAll(directory, 0755) 153 | if err != nil { 154 | return err 155 | } 156 | 157 | key, err := GenerateCAKey() 158 | if err != nil { 159 | return err 160 | } 161 | cert, err := GenerateCACert(key) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | keyfile := directory + "/ca.key" 167 | certfile := directory + "/ca.crt" 168 | 169 | err = writeFileExcl(keyfile, key, 0600) 170 | if err != nil { 171 | return err 172 | } 173 | return writeFileExcl(certfile, cert, 0644) 174 | } 175 | 176 | // write data to file, which must not already exist 177 | func writeFileExcl(filename string, data []byte, perm os.FileMode) error { 178 | kf, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm) 179 | if err != nil { 180 | return err 181 | } 182 | defer kf.Close() 183 | _, err = kf.Write(data) 184 | return err 185 | } 186 | -------------------------------------------------------------------------------- /ca/store_test.go: -------------------------------------------------------------------------------- 1 | package ca_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/syncsynchalt/dime-a-tap/test" 6 | "io/ioutil" 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | "github.com/syncsynchalt/dime-a-tap/ca" 12 | ) 13 | 14 | var testCACert string = `-----BEGIN CERTIFICATE----- 15 | MIIDJTCCAg2gAwIBAgIBATANBgkqhkiG9w0BAQsFADA0MQswCQYDVQQGEwJVUzET 16 | MBEGA1UEChMKRGltZS1BLVRhcDEQMA4GA1UECxMHRmFrZSBDQTAeFw0xODEwMDMx 17 | OTU2MjVaFw0yODEwMDMxOTU2MjVaMDQxCzAJBgNVBAYTAlVTMRMwEQYDVQQKEwpE 18 | aW1lLUEtVGFwMRAwDgYDVQQLEwdGYWtlIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC 19 | AQ8AMIIBCgKCAQEA1RyUGMGSuoXqzFkK9SDTFjiBB6aglWpF2h6nlWXZeSxG1eGk 20 | RmnXR4VNCefsscKlav2eAn/pWMTGQ7PeQOUqPBq3sQy94dr7VNWt34uQ58Q2ffWc 21 | 4ay3znDeFCuPf/PiabFmk+ktm/AEGRLJHP9A15gUJ9bjEomNdyJI0EKvOXu2rkZT 22 | +qBDxSwTVKdJ7IaahuYAB7om8lyEqqAevUhCMNRsTG4I9Ea4k4Dg8n+gY81ABJIL 23 | fSqSWLWhlFUMvmc6PiX0SdF/mwcs+KZsOFoua2rYGrkyjMyPhfYjaTKXnbhGC9mU 24 | 6a1B7wm3vl+Bu6a/Lht7sI71VdnJ4I0IajSEjQIDAQABo0IwQDAOBgNVHQ8BAf8E 25 | BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUNVJTMZdEd9AHuWum+tvx 26 | 8ZgAFyowDQYJKoZIhvcNAQELBQADggEBAAvKkMyESOC41P7Bqb6af1xdiNfuA/+Y 27 | XSRbHaaqBjz/3cqKogwpMTqUROdkOIw8QqF+BarjbfU2os2m5saCu4tW7VEk2IUX 28 | zUY/BgTYOa4b+NvA4jdr37MtCZ3r6E+fE1i6S3cxy4mX9ZMY8Tm94FoOKc1qhqn1 29 | yNNzGLEahQ8Qf2GqfNFcIg4MERzohPnP4X/JJD4PHQ2mXxPKoXSgP/eVIZt1h2vg 30 | JjY2Ky2gFYTDwSCN0wctLRCPwHp7YMVFB/u79VeCg7MyHss4palpkh+YLPhgEFOi 31 | 0Sa1nCRk4/av/sn5HwzzRkjaZDNcPpHhcTvpsi1eYc05KFK25U0viuk= 32 | -----END CERTIFICATE----- 33 | ` 34 | 35 | var testCAKey string = `-----BEGIN RSA PRIVATE KEY----- 36 | MIIEowIBAAKCAQEA1RyUGMGSuoXqzFkK9SDTFjiBB6aglWpF2h6nlWXZeSxG1eGk 37 | RmnXR4VNCefsscKlav2eAn/pWMTGQ7PeQOUqPBq3sQy94dr7VNWt34uQ58Q2ffWc 38 | 4ay3znDeFCuPf/PiabFmk+ktm/AEGRLJHP9A15gUJ9bjEomNdyJI0EKvOXu2rkZT 39 | +qBDxSwTVKdJ7IaahuYAB7om8lyEqqAevUhCMNRsTG4I9Ea4k4Dg8n+gY81ABJIL 40 | fSqSWLWhlFUMvmc6PiX0SdF/mwcs+KZsOFoua2rYGrkyjMyPhfYjaTKXnbhGC9mU 41 | 6a1B7wm3vl+Bu6a/Lht7sI71VdnJ4I0IajSEjQIDAQABAoIBAQC6fhrfmy4TCjQS 42 | BW4AW2w9ys6nalqmxmxAV4khxRJN5sBKVP6UG/UngnCLVakdWg+2FCEdYOBMLU6v 43 | Wo0JT0HpfRv41QSpzB8a+y8ALDtvhpaFHdXe622iO8Ur837NYxhkk7kHgQvHpX+A 44 | jZ7vQDR3Nn+U6Yim5Tal5ZvAnEqIyrA6CwKW/8wsAl2E8CVvugjzQ2UKhfOUbarM 45 | iQEMw4sBe1OL8beZ6EH9hWxxgvB3GLubVIAZlYUtjkVsh6m18pBl5XvTFXiJSgd4 46 | Wdmzj75vHfa3b0G/Lppem5FHPnfgi6kWb7KM+IdMAZoTCoEbNzxqc7LGAexS3u6n 47 | 48R30bFlAoGBAPQ5XLaPitdBcWi03uGblO9DypJa5vPKkTUm5Sbv02gZvapsPPrG 48 | 9uru5yPVDJNSYTm7RKLf5YidxPiR61lDNGFRGpvuiRLjxYledmFhNzIXJujVpuYU 49 | E+5Gbq7RLaFrmanGzQ7TL9UdIKra6ZRmwKYO//gyVtB3sIWdSgH5xl/XAoGBAN9j 50 | LC8vqR+0EIrD3DvXDPB+US8Tckl+qXTiVM7l22lvJw3nys1SDaxl5F5AfPJ/7OqY 51 | 8/CiEMA1Jp0PlcL437+zBUsmebzAdUbVy4cQf4qVsW93d5yG3FwSl8ntkW9hvssc 52 | nghpp06iPhWRT9U1+PWGSnHE0tsVRhvnAyN0kUI7AoGAfTMO6XQKzDD7b58Rh3zX 53 | zBTnu0GoliApcqMe5Ggb64kOp1hXpoPrPyL8EW19xeR8fTkYhZrcM74VpQxBJ4CB 54 | UMZgKsINOUbVFIf9jgxlXGNsCf7FUbvHP+aRhUMs7kyX+OY2ZzwykEEfZxdUmURX 55 | zIlyBY3g3XwOXWD1+K9QV/8CgYBeZ1rU1h9y9nXHLt5zq34cZEWKz30M8ipK6xtM 56 | FHeVJxQqHDroajS9FpJcAoTLNqS4v8rXdqX9lHitB1kS/HoSWWVzTN9FlU/6j39j 57 | pOVBe+FwadxymcumXXUoMO21VGl9DKr8gynhYU87bh1+zUBZAleTnMo/K85lHEuH 58 | QEvi4QKBgH5i2mftXBFhuTvR9X2+E0vO8cLbp1OGXVEfHAfVe4mxYblTj+VGjaAm 59 | DL+c+tuNGLeYoe3RF0C3gZQqwiBZVK7mnjJU4nfJg8bEifOuzTeYe90wK1hKztxq 60 | r05hVbaT7+DBl6NLVjl4iCBQnS5CTc0jggs0U0z6McyMB+p0ErwU 61 | -----END RSA PRIVATE KEY----- 62 | ` 63 | 64 | func TestStoreBadDomains(t *testing.T) { 65 | store, err := ca.NewStore("") 66 | test.Ok(t, err) 67 | _, _, err = store.GetCertificate("") 68 | test.Equals(t, "domain is not valid", err.Error()) 69 | 70 | _, _, err = store.GetCertificate(".a") 71 | test.Equals(t, "domain .a is not valid", err.Error()) 72 | 73 | _, _, err = store.GetCertificate("a_z") 74 | test.Equals(t, "domain a_z is not valid", err.Error()) 75 | } 76 | 77 | func looksLikeRsaKey(t *testing.T, pem []byte) { 78 | test.CallerDepth++ 79 | defer func() { test.CallerDepth-- }() 80 | prefix := "-----BEGIN RSA PRIVATE KEY-----\nM" 81 | test.Equals(t, prefix, string(pem)[:len(prefix)]) 82 | } 83 | 84 | func looksLikeCertificate(t *testing.T, pem []byte) { 85 | test.CallerDepth++ 86 | defer func() { test.CallerDepth-- }() 87 | prefix := "-----BEGIN CERTIFICATE-----\nM" 88 | test.Equals(t, prefix, string(pem)[:len(prefix)]) 89 | } 90 | 91 | func TestStoreMemCache(t *testing.T) { 92 | store, err := ca.NewStore("") 93 | test.Ok(t, err) 94 | key1, cert1, err := store.GetCertificate("a") 95 | test.Ok(t, err) 96 | looksLikeRsaKey(t, key1) 97 | looksLikeCertificate(t, cert1) 98 | 99 | key2, cert2, err := store.GetCertificate("a") 100 | test.Ok(t, err) 101 | test.Equals(t, key1, key2) 102 | test.Equals(t, cert1, cert2) 103 | } 104 | 105 | func TestStoreSmashCase(t *testing.T) { 106 | store, err := ca.NewStore("") 107 | test.Ok(t, err) 108 | key1, cert1, err := store.GetCertificate("a") 109 | test.Ok(t, err) 110 | 111 | key2, cert2, err := store.GetCertificate("A") 112 | test.Ok(t, err) 113 | 114 | test.Equals(t, key1, key2) 115 | test.Equals(t, cert1, cert2) 116 | } 117 | 118 | func TestStoreFileStore(t *testing.T) { 119 | mydir := fmt.Sprintf("/tmp/golang.test.%d", time.Now().UnixNano()) 120 | defer os.RemoveAll(mydir) 121 | ca.CreateCAStore(mydir) 122 | 123 | store, err := ca.NewStore(mydir) 124 | test.Ok(t, err) 125 | key1, cert1, err := store.GetCertificate("a.com") 126 | test.Ok(t, err) 127 | 128 | key2, err := ioutil.ReadFile(mydir + "/domain-a.com.key") 129 | test.Ok(t, err) 130 | cert2, err := ioutil.ReadFile(mydir + "/domain-a.com.crt") 131 | test.Ok(t, err) 132 | test.Equals(t, key1, key2) 133 | test.Equals(t, cert1, cert2) 134 | 135 | key3, cert3, err := store.GetCertificate("a.com") 136 | test.Ok(t, err) 137 | test.Equals(t, key1, key3) 138 | test.Equals(t, cert1, cert3) 139 | 140 | key4, err := ioutil.ReadFile(mydir + "/domain-a.com.key") 141 | test.Ok(t, err) 142 | cert4, err := ioutil.ReadFile(mydir + "/domain-a.com.crt") 143 | test.Ok(t, err) 144 | test.Equals(t, key1, key4) 145 | test.Equals(t, cert1, cert4) 146 | } 147 | --------------------------------------------------------------------------------