diff --git a/README.md b/README.md new file mode 100644 index 0000000..61c47a3 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# CAcert signer in Go + +This is a reimplementation of the CAcert signer code from the +[CommModule directory](https://git.cacert.org/gitweb/?p=cacert-devel.git;a=tree;f=CommModule) of the CAcert software +in [Go](https://golang.org/). The goal of this effort is to provide a more maintainable version of the software. + +## Running the signer and client locally + +The signer is usually attached to a USB serial port. You can run the signer and client locally using the +[socat](https://manpages.debian.org/buster/socat/socat.1.en.html) utility. + +```shell script +socat -d -d PTY,link=$(pwd)/ttyS0 PTY,link=$(pwd)/ttyS1 +``` + +You may then run the server + +```shell script +go run signer/main.go -a $(pwd)/dev/ttyS0 +``` + +and the client: + +```shell script +go run client/main.go -a $(pwd)/dev/ttyS1 +``` \ No newline at end of file diff --git a/client/main.go b/client/main.go index cdd0a14..67b4237 100644 --- a/client/main.go +++ b/client/main.go @@ -1,71 +1,132 @@ package main import ( + "encoding/binary" + "errors" "flag" + "fmt" "git.cacert.org/cacert-gosigner/datastructures" "git.cacert.org/cacert-gosigner/shared" - "log" + "io/ioutil" "time" "github.com/goburrow/serial" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" ) -var ( - address string - baudrate int - databits int - stopbits int - parity string - message string -) +type SerialConfig struct { + Address string `yaml:"address"` + BaudRate int `yaml:"baudrate"` + DataBits int `yaml:"databits"` + StopBits int `yaml:"stopbits"` + Parity string `yaml:"parity"` +} + +type ClientConfig struct { + Serial SerialConfig `yaml:"serial_config"` + Paranoid bool `yaml:"paranoid"` + Debug bool `yaml:"debug"` + GNUPGBinary string `yaml:"gnupg_bin"` + OpenSSLBinary string `yaml:"openssl_bin"` + MySQLDSN string `yaml:"mysql_dsn"` +} + +var defaultConfig = ClientConfig{ + Serial: SerialConfig{ + Address: "/dev/ttyUSB0", + BaudRate: 115200, + DataBits: 8, + StopBits: 1, + Parity: "N", + }, + Paranoid: false, + Debug: false, + OpenSSLBinary: "/usr/bin/openssl", + GNUPGBinary: "/usr/bin/gpg", + MySQLDSN: ":@/database?parseTime=true", +} + +const HandshakeByte = 0x02 +const AckByte = 0x10 +const NackByte = 0x11 + +func readConfig(configFile string) (config *ClientConfig, err error) { + source, err := ioutil.ReadFile(configFile) + if err != nil { + log.Errorf("opening configuration file failed: %v", err) + if exampleConfig, err := generateExampleConfig(configFile); err != nil { + return nil, err + } else { + log.Info("starting with default config") + return exampleConfig, nil + } + } + if err := yaml.Unmarshal(source, &config); err != nil { + return nil, fmt.Errorf("loading configuration file failed: %v", err) + } + + return config, nil +} + +func generateExampleConfig(configFile string) (config *ClientConfig, err error) { + config = &defaultConfig + configBytes, err := yaml.Marshal(config) + if err != nil { + log.Errorf("could not generate configuration data") + return + } + + log.Infof("example data for %s:\n\n---\n%s\n", configFile, configBytes) + return +} func main() { - flag.StringVar(&address, "a", "/dev/ttyUSB0", "address") - flag.IntVar(&baudrate, "b", 115200, "baud rate") - flag.IntVar(&databits, "d", 8, "data bits") - flag.IntVar(&stopbits, "s", 1, "stop bits") - flag.StringVar(&parity, "p", "N", "parity (N/E/O)") - flag.StringVar(&message, "m", "serial", "message") + var configFile string + + flag.StringVar(&configFile, "c", "client.yaml", "client configuration file in YAML format") flag.Parse() - config := serial.Config{ - Address: address, - BaudRate: baudrate, - DataBits: databits, - StopBits: stopbits, - Parity: parity, + var clientConfig *ClientConfig + var serialConfig *serial.Config + var err error + + if clientConfig, err = readConfig(configFile); err != nil { + log.Panic(err) + } + serialConfig = &serial.Config{ + Address: clientConfig.Serial.Address, + BaudRate: clientConfig.Serial.BaudRate, + DataBits: clientConfig.Serial.DataBits, + StopBits: clientConfig.Serial.StopBits, + Parity: clientConfig.Serial.Parity, Timeout: 30 * time.Second, } - log.Printf("connecting %+v", config) - port, err := serial.Open(&config) + if clientConfig.Debug { + log.SetLevel(log.DebugLevel) + } + + log.Debugf("connecting %+v", serialConfig) + port, err := serial.Open(serialConfig) if err != nil { log.Fatal(err) } - log.Println("connected") + log.Debug("connected") defer func() { err := port.Close() if err != nil { log.Fatal(err) } - log.Println("closed") + log.Debug("closed") }() - if _, err = port.Write([]byte{0x02}); err != nil { - log.Fatal(err) - } - - resp := make([]byte, 1) - if _, err = port.Read(resp); err != nil { - log.Fatal(err) - } - request := datastructures.NewNulRequest() timeout := time.After(2700 * time.Millisecond) errorChannel := make(chan error, 1) responseChannel := make(chan *datastructures.SignerResponse, 1) go func() { - if response, err := SendRequest(&port, request); err != nil { + if response, err := SendRequest(port, request); err != nil { errorChannel <- err } else { responseChannel <- response @@ -77,30 +138,133 @@ func main() { log.Fatal("timeout") case err := <-errorChannel: log.Fatal(err) - case response := <- responseChannel: + case response := <-responseChannel: if err := Process(response); err != nil { log.Fatal(err) } } - requestBytes := request.Serialize() - - if _, err = port.Write(requestBytes); err != nil { - log.Fatal(err) - } - - if _, err = port.Write([]byte{datastructures.CalculateXorCheckSum([][]byte{requestBytes})}); err != nil { - log.Fatal(err) - } - - if _, err = port.Write([]byte(shared.MagicTrailer)); err != nil { - log.Fatal(err) - } } func Process(response *datastructures.SignerResponse) error { + log.Debugf("process %v", response) return nil } -func SendRequest(port *serial.Port, request *datastructures.SignerRequest) (*datastructures.SignerResponse, error) { +func sendHandShake(port serial.Port) error { + log.Debug("Shaking hands ...") + if length, err := port.Write([]byte{HandshakeByte}); err != nil { + return fmt.Errorf("could not write handshake byte: %v", err) + } else { + log.Debugf("wrote %d handshake bytes", length) + } + handShakeResponse := make([]byte, 1) + if length, err := port.Read(handShakeResponse); err != nil { + return fmt.Errorf("failed to read handshake response: %v", err) + } else { + log.Debugf("read %d bytes", length) + } + if handShakeResponse[0] != AckByte { + return fmt.Errorf("invalid handshake response expected 0x10 received %x", handShakeResponse[0]) + } + log.Debug("Handshake successful") + return nil +} + +func receiveResponse(port *serial.Port, responseChan *chan []byte, errorChan *chan error) { + header, err := shared.ReceiveBytes(port, 1, 20) + if err != nil { + *errorChan <- err + return + } + if header[0] != HandshakeByte { + *errorChan <- fmt.Errorf("unexpected byte 0x%x expected 0x%x", header, HandshakeByte) + } + + if _, err := (*port).Write([]byte{AckByte}); err != nil { + *errorChan <- errors.New("could not write ACK") + return + } + + lengthBytes, err := shared.ReceiveBytes(port, 3, 2) + if err != nil { + *errorChan <- err + return + } + blockLength := binary.BigEndian.Uint32([]byte{0x0, lengthBytes[0], lengthBytes[1], lengthBytes[2]}) + blockData, err := shared.ReceiveBytes(port, int(blockLength), 5) + if err != nil { + *errorChan <- err + return + } + + checkSum, err := shared.ReceiveBytes(port, 1, 2) + if err != nil { + *errorChan <- err + return + } + log.Debugf("block checksum is %d", checkSum) + trailer, err := shared.ReceiveBytes(port, len(shared.MagicTrailer), 2) + if err != nil { + *errorChan <- err + return + } + if string(trailer) != shared.MagicTrailer { + *errorChan <- errors.New("expected trailer bytes not found") + return + } + + if _, err := (*port).Write([]byte{AckByte}); err != nil { + *errorChan <- fmt.Errorf("could not write ACK byte: %v", err) + } + *responseChan <- blockData + + return +} + +func SendRequest(port serial.Port, request *datastructures.SignerRequest) (response *datastructures.SignerResponse, err error) { + log.Debugf("send request %v to serial port %v", request, port) + + if err = sendHandShake(port); err != nil { + return nil, err + } + + requestBytes := request.Serialize() + if length, err := port.Write(requestBytes); err != nil { + log.Fatal(err) + } else { + log.Debugf("wrote %d request bytes", length) + } + + if length, err := port.Write([]byte{datastructures.CalculateXorCheckSum([][]byte{requestBytes})}); err != nil { + log.Fatal(err) + } else { + log.Debugf("wrote %d checksum bytes", length) + } + + if length, err := port.Write([]byte(shared.MagicTrailer)); err != nil { + log.Fatal(err) + } else { + log.Debugf("wrote %d trailer bytes", length) + } + + header, err := shared.ReceiveBytes(&port, 1, 20) + if err != nil { + return nil, err + } + if header[0] != AckByte { + return nil, fmt.Errorf("unexpected byte 0x%x expected 0x%x", header, AckByte) + } + + responseChan := make(chan []byte, 1) + errChan := make(chan error, 1) + go receiveResponse(&port, &responseChan, &errChan) + + select { + case responseData := <-responseChan: + log.Debugf("response data: %v", responseData) + case err := <-errChan: + log.Errorf("%v", err) + } + return nil, nil } diff --git a/datastructures/signerrequest.go b/datastructures/signerrequest.go index 6566043..c15c461 100644 --- a/datastructures/signerrequest.go +++ b/datastructures/signerrequest.go @@ -22,6 +22,8 @@ type SignerRequest struct { Content3 string } +const protocolVersion = 1 + func SignerRequestFromData(lengthBytes []byte, blockData []byte, checkSum byte) (*SignerRequest, error) { headerLength := decode24BitLength(blockData[0:3]) headerBytes := blockData[3 : 3+headerLength] @@ -76,5 +78,9 @@ func (r SignerRequest) Serialize() []byte { } func NewNulRequest() *SignerRequest { - return &SignerRequest{Version: 1, Action: ActionNul, Content1: time.Now().UTC().Format("010203042006.05")} + return &SignerRequest{ + Version: protocolVersion, + Action: ActionNul, + Content1: time.Now().UTC().Format("010203042006.05"), + } } diff --git a/shared/io.go b/shared/io.go new file mode 100644 index 0000000..e0d46f8 --- /dev/null +++ b/shared/io.go @@ -0,0 +1,31 @@ +package shared + +import ( + "errors" + "github.com/goburrow/serial" + "io" + "time" +) + +// receive the requested number of bytes from serial port and stop after the given timeout in seconds +func ReceiveBytes(port *serial.Port, count int, timeout time.Duration) ([]byte, error) { + timeoutCh := time.After(timeout * time.Second) + readCh := make(chan []byte, 1) + errCh := make(chan error, 1) + go func() { + data := make([]byte, count) + if _, err := io.ReadAtLeast(*port, data, count); err != nil { + errCh <- err + } else { + readCh <- data + } + }() + select { + case <-timeoutCh: + return nil, errors.New("timeout") + case err := <-errCh: + return nil, err + case data := <-readCh: + return data, nil + } +} diff --git a/shared/shared.go b/shared/shared.go index da345c8..d0d6c12 100644 --- a/shared/shared.go +++ b/shared/shared.go @@ -1,4 +1,3 @@ package shared const MagicTrailer = "rie4Ech7" - diff --git a/signer/main.go b/signer/main.go index 2274e21..3e9e57d 100644 --- a/signer/main.go +++ b/signer/main.go @@ -7,7 +7,6 @@ import ( "fmt" "git.cacert.org/cacert-gosigner/datastructures" "git.cacert.org/cacert-gosigner/shared" - "io" "log" "time" @@ -70,7 +69,7 @@ readLoop: if err != nil { log.Printf("ERROR %v\n", err) } else { - SendResponse(&port, response) + _ = SendResponse(&port, response) } case <-timeout: log.Println("timeout in main loop") @@ -90,7 +89,7 @@ func SendResponse(port *serial.Port, response *datastructures.SignerResponse) er return err } - if ack, err := receiveBytes(port, 1, 5); err != nil { + if ack, err := shared.ReceiveBytes(port, 1, 5); err != nil { return err } else if ack[0] != 0x10 { return errors.New(fmt.Sprintf("invalid ack byte 0x%02x", ack[0])) @@ -112,7 +111,7 @@ func SendResponse(port *serial.Port, response *datastructures.SignerResponse) er return err } - if ack, err := receiveBytes(port, 1, 5); err != nil { + if ack, err := shared.ReceiveBytes(port, 1, 5); err != nil { return err } else if ack[0] == 0x10 { tryAgain = false @@ -146,13 +145,13 @@ func handleNulAction(command datastructures.SignerRequest) (*datastructures.Sign // Receive a request and generate a request data structure func Receive(port *serial.Port, commandChan *chan datastructures.SignerRequest, errorChan *chan error) { - header, err := receiveBytes(port, 1, 20) + header, err := shared.ReceiveBytes(port, 1, 20) if err != nil { *errorChan <- err return } if header[0] != 0x02 { - *errorChan <- errors.New(fmt.Sprintf("unexpected byte 0x%x expected 0x02", header)) + *errorChan <- fmt.Errorf("unexpected byte 0x%x expected 0x02", header) } if _, err := (*port).Write([]byte{0x10}); err != nil { @@ -160,19 +159,19 @@ func Receive(port *serial.Port, commandChan *chan datastructures.SignerRequest, return } - lengthBytes, err := receiveBytes(port, 3, 2) + lengthBytes, err := shared.ReceiveBytes(port, 3, 2) if err != nil { *errorChan <- err return } blockLength := binary.BigEndian.Uint32([]byte{0x0, lengthBytes[0], lengthBytes[1], lengthBytes[2]}) - blockData, err := receiveBytes(port, int(blockLength), 5) + blockData, err := shared.ReceiveBytes(port, int(blockLength), 5) if err != nil { *errorChan <- err return } - checkSum, err := receiveBytes(port, 1, 2) + checkSum, err := shared.ReceiveBytes(port, 1, 2) if err != nil { *errorChan <- err return @@ -183,7 +182,7 @@ func Receive(port *serial.Port, commandChan *chan datastructures.SignerRequest, *errorChan <- err } - trailer, err := receiveBytes(port, len(shared.MagicTrailer), 2) + trailer, err := shared.ReceiveBytes(port, len(shared.MagicTrailer), 2) if err != nil { *errorChan <- err return @@ -201,25 +200,3 @@ func Receive(port *serial.Port, commandChan *chan datastructures.SignerRequest, *commandChan <- *command } -// receive the requested number of bytes from serial port and stop after the given timeout in seconds -func receiveBytes(port *serial.Port, count int, timeout time.Duration) ([]byte, error) { - timeoutCh := time.After(timeout * time.Second) - readCh := make(chan []byte, 1) - errCh := make(chan error, 1) - go func() { - data := make([]byte, count) - if _, err := io.ReadAtLeast(*port, data, count); err != nil { - errCh <- err - } else { - readCh <- data - } - }() - select { - case <-timeoutCh: - return nil, errors.New("timeout") - case err := <-errCh: - return nil, err - case data := <-readCh: - return data, nil - } -}