From 3107ad8abb44e1c111fa31bee637387c55fc10e6 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Wed, 3 Aug 2022 14:38:36 +0200 Subject: [PATCH] Implement serial link and protocol handling infrastructure This commit adds basic serial link and protocol support. None of the commands from the docs/design.md document is implemented yet. The following new packages have been added: - seriallink containing the serial link handler including COBS decoding and encoding - protocol containing the protocol handler including msgpack unmarshalling and marshaling - health containing a rudimentary health check implementation - messages containing command and response types and generated msgpack marshaling code A client simulation command has been added in cmd/clientsim. README.md got instructions how to run the client simulator. The docs/config.sample.yaml contains a new section for the serial connection parameters. --- .gitignore | 3 + README.md | 21 +- cmd/clientsim/main.go | 144 +++++++++ cmd/signer/main.go | 21 ++ docs/config.sample.yaml | 6 + go.mod | 8 +- go.sum | 17 +- pkg/config/config.go | 34 +++ pkg/config/config_test.go | 10 + pkg/health/health.go | 27 ++ pkg/hsm/context_test.go | 2 + pkg/hsm/hsm_test.go | 2 + pkg/messages/messages.go | 68 +++++ pkg/messages/resolver.msgpackgen.go | 439 ++++++++++++++++++++++++++++ pkg/protocol/protocol.go | 129 ++++++++ pkg/seriallink/seriallink.go | 134 +++++++++ 16 files changed, 1058 insertions(+), 7 deletions(-) create mode 100644 cmd/clientsim/main.go create mode 100644 pkg/health/health.go create mode 100644 pkg/messages/messages.go create mode 100644 pkg/messages/resolver.msgpackgen.go create mode 100644 pkg/protocol/protocol.go create mode 100644 pkg/seriallink/seriallink.go diff --git a/.gitignore b/.gitignore index b623959..1eda5c5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ *.pem *.pub .idea/ +/clientsim +/signer +/testPty ca-hierarchy.json config.yaml dist/ \ No newline at end of file diff --git a/README.md b/README.md index 554fa37..f15cbb1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Setup HSM keys and certificates -``` +```shell sudo apt install softhsm2 umask 077 mkdir -p ~/.config/softhsm2/tokens @@ -17,7 +17,24 @@ go run ./cmd/signer -setup ## Run the signer -``` +```shell export PKCS11_PIN_LOCALHSM=123456 go run ./cmd/signer ``` + +## Run the client simulator with socat + +You may run the client simulator that sends commands via `stdout` and reads responses on `stdin` via `socat` to +simulate traffic on an emulated serial device: + +```shell +sudo apt install socat +``` + +```shell +go build ./cmd/clientsim +socat -d -d -v pty,rawer,link=$(pwd)/testPty EXEC:./clientsim,pty,rawer +``` + +You will need to configure `$(pwd)/testPty` as `serial`/`device` in your `config.yaml` to let the signer command find +the emulated serial device. \ No newline at end of file diff --git a/cmd/clientsim/main.go b/cmd/clientsim/main.go new file mode 100644 index 0000000..aefe282 --- /dev/null +++ b/cmd/clientsim/main.go @@ -0,0 +1,144 @@ +/* +Copyright 2022 CAcert Inc. +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// client simulator + +package main + +import ( + "fmt" + "log" + "os" + "sync" + "time" + + "github.com/justincpresley/go-cobs" + "github.com/shamaton/msgpackgen/msgpack" + + "git.cacert.org/cacert-gosigner/pkg/messages" +) + +const cobsDelimiter = 0x00 + +var cobsConfig = cobs.Config{SpecialByte: cobsDelimiter, Delimiter: true, EndingSave: true} + +func main() { + const ( + bufferSize = 1024 * 1024 + readInterval = 50 * time.Millisecond + ) + + errors := make(chan error) + + errorLog := log.New(os.Stderr, "", log.LstdFlags) + + wg := sync.WaitGroup{} + wg.Add(1) + + done := make(chan struct{}) + frame := make(chan []byte) + + go func(done chan struct{}) { + buf := make([]byte, bufferSize) + + for { + select { + case <-done: + wg.Done() + + return + + default: + count, err := os.Stdin.Read(buf) + if err != nil { + errors <- err + + wg.Done() + + return + } + + if count == 0 { + time.Sleep(readInterval) + + continue + } + + data := buf[:count] + + err = cobs.Verify(data, cobsConfig) + if err != nil { + errors <- err + + wg.Done() + + return + } + + frame <- cobs.Decode(data, cobsConfig) + } + } + }(done) + + err := writeTestCommands(frame, errorLog) + if err != nil { + errorLog.Printf("could not write test commands") + } + + err = <-errors + if err != nil { + errorLog.Printf("error: %v", err) + } + + wg.Wait() +} + +func writeTestCommands(responses chan []byte, errorLog *log.Logger) error { + messages.RegisterGeneratedResolver() + + commands := []messages.Command{ + { + Code: messages.CmdHealth, + TimeStamp: time.Now().UTC(), + }, + } + + for _, command := range commands { + commandBytes, err := msgpack.Marshal(command) + if err != nil { + return fmt.Errorf("could not marshal command bytes: %w", err) + } + + _, err = os.Stdout.Write(cobs.Encode(commandBytes, cobsConfig)) + if err != nil { + return fmt.Errorf("write failed: %w", err) + } + + responseBytes := <-responses + + var response messages.Response + + err = msgpack.Unmarshal(responseBytes, &response) + if err != nil { + return fmt.Errorf("could not unmarshal msgpack data: %w", err) + } + + errorLog.Printf("received response: %+v", response) + } + + return nil +} diff --git a/cmd/signer/main.go b/cmd/signer/main.go index 7cdbeef..842941f 100644 --- a/cmd/signer/main.go +++ b/cmd/signer/main.go @@ -23,7 +23,10 @@ import ( "os" "git.cacert.org/cacert-gosigner/pkg/config" + "git.cacert.org/cacert-gosigner/pkg/health" "git.cacert.org/cacert-gosigner/pkg/hsm" + "git.cacert.org/cacert-gosigner/pkg/protocol" + "git.cacert.org/cacert-gosigner/pkg/seriallink" ) var ( @@ -97,5 +100,23 @@ func main() { return } + healthHandler := health.New(version) + + proto, err := protocol.New(infoLog, errorLog, protocol.RegisterHealthHandler(healthHandler)) + if err != nil { + errorLog.Fatalf("could not setup protocol handler: %v", err) + } + + serialHandler, err := seriallink.New(caConfig.GetSerial(), proto) + if err != nil { + errorLog.Fatalf("could not setup serial link handler: %v", err) + } + + defer func() { _ = serialHandler.Close() }() + + if err = serialHandler.Run(); err != nil { + errorLog.Fatalf("error in serial handler: %v", err) + } + infoLog.Print("setup complete, starting signer operation") } diff --git a/docs/config.sample.yaml b/docs/config.sample.yaml index 2309743..553a81d 100644 --- a/docs/config.sample.yaml +++ b/docs/config.sample.yaml @@ -24,6 +24,12 @@ Settings: ocsp: "http://ocsp.cacert.org/" crl: "http://crl.cacert.org/%s.crl" issuer: "http://www.cacert.org/certs/%s.crt" + # Settings for the serial link + # baud and timeout-millis are optional + serial: + device: "/dev/ttyUSB0" + baud: 115200 + timeout-millis: 5000 # KeyStorage defines PKCS#11 tokens, a token named 'default' must be present KeyStorage: diff --git a/go.mod b/go.mod index 5470dda..13d9fbd 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,12 @@ go 1.17 require ( github.com/ThalesIgnite/crypto11 v1.2.5 - github.com/stretchr/testify v1.7.1 + github.com/justincpresley/go-cobs v1.2.0 + github.com/shamaton/msgpackgen v0.3.0 + github.com/stretchr/testify v1.8.0 + github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -14,6 +17,7 @@ require ( github.com/miekg/pkcs11 v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/shamaton/msgpack/v2 v2.1.0 // indirect github.com/thales-e-security/pool v0.0.2 // indirect golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect ) diff --git a/go.sum b/go.sum index 20201a2..6436e29 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,11 @@ github.com/ThalesIgnite/crypto11 v1.2.5 h1:1IiIIEqYmBvUYFeMnHqRft4bwf/O36jryEUpY+9ef8E= github.com/ThalesIgnite/crypto11 v1.2.5/go.mod h1:ILDKtnCKiQ7zRoNxcp36Y1ZR8LBPmR2E23+wTQe/MlE= +github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/justincpresley/go-cobs v1.2.0 h1:dyWszWzXObEv8sxVMJTAIo9XT7HEM10vkAOZq2eVEsQ= +github.com/justincpresley/go-cobs v1.2.0/go.mod h1:L0d+EbGirv6IzsXNzwULduI2/z3ijkkAmsAuPMpLfqA= github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= @@ -11,10 +14,18 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/shamaton/msgpack/v2 v2.1.0 h1:9jJ2eGZw2Wa9KExPX3KaDDckVjgr4zhXGFCfWagUWqg= +github.com/shamaton/msgpack/v2 v2.1.0/go.mod h1:aTUEmh31ziGX1Ml7wMPLVY0f4vT3CRsCvZRoSCs+VGg= +github.com/shamaton/msgpackgen v0.3.0 h1:q6o7prOEJFdF9BAPgkOtfzJbs55pQi7g44RUnEVUxtM= +github.com/shamaton/msgpackgen v0.3.0/go.mod h1:fd99fDDuxuTiWzkHC59uEGzrt/WDu+ltGZTbEWwVXIc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07 h1:UyzmZLoiDWMRywV4DUYb9Fbt8uiOSooupjTq10vpvnU= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= @@ -24,5 +35,5 @@ golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuX gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/config/config.go b/pkg/config/config.go index 6ae4851..435529d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -31,6 +31,12 @@ import ( "gopkg.in/yaml.v3" ) +type Serial struct { + Device string + Baud int + Timeout time.Duration +} + type Settings struct { Organization *pkix.Name ValidityYears struct { @@ -39,6 +45,7 @@ type Settings struct { URLPatterns struct { Ocsp, CRL, Issuer string } + Serial *Serial } type SettingsError struct { @@ -67,6 +74,11 @@ func (s *Settings) UnmarshalYAML(n *yaml.Node) error { CRL string `yaml:"crl"` Issuer string `yaml:"issuer"` } `yaml:"url-patterns"` + Serial struct { + Device string `yaml:"device"` + Baud int `yaml:"baud"` + TimeoutMillis int `yaml:"timeout-millis"` + } `yaml:"serial"` }{} err := n.Decode(&data) @@ -111,6 +123,18 @@ func (s *Settings) UnmarshalYAML(n *yaml.Node) error { return SettingsError{"url-pattern 'issuer' must contain one '%s' placeholder"} } + if data.Serial.Device == "" { + return SettingsError{"you must specify a serial 'device'"} + } + + if data.Serial.Baud == 0 { + data.Serial.Baud = 115200 + } + + if data.Serial.TimeoutMillis == 0 { + data.Serial.TimeoutMillis = 5000 + } + s.Organization = &pkix.Name{} s.Organization.Organization = data.Organization.Organization s.Organization.Country = data.Organization.Country @@ -125,6 +149,12 @@ func (s *Settings) UnmarshalYAML(n *yaml.Node) error { s.URLPatterns.CRL = data.URLPatterns.CRL s.URLPatterns.Issuer = data.URLPatterns.Issuer + s.Serial = &Serial{ + Device: data.Serial.Device, + Baud: data.Serial.Baud, + Timeout: time.Duration(data.Serial.TimeoutMillis) * time.Millisecond, + } + return nil } @@ -287,6 +317,10 @@ func (c *SignerConfig) GetKeyStorage(label string) (*KeyStorage, error) { return keyStorage, nil } +func (c *SignerConfig) GetSerial() *Serial { + return c.global.Serial +} + // LoadConfiguration reads YAML configuration from the given reader as a SignerConfig structure func LoadConfiguration(r io.Reader) (*SignerConfig, error) { config := struct { diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index c5beea0..658750a 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -421,6 +421,8 @@ Settings: ocsp: http://ocsp.example.org/ crl: http://crl.example.org/%s issuer: http://%s.cas.example.org/ + serial: + device: /dev/ttyUSB0 KeyStorage: default: type: softhsm @@ -476,6 +478,8 @@ Settings: ocsp: http://ocsp.example.org/ crl: http://crl.example.org/%s.crl issuer: http://%s.cas.example.org/ + serial: + device: /dev/ttyUSB0 CAs: root: common-name: "Root CA" @@ -497,6 +501,8 @@ Settings: ocsp: http://ocsp.example.org/ crl: http://crl.example.org/%s.crl issuer: http://%s.cas.example.org/ + serial: + device: /dev/ttyUSB0 KeyStorage: default: label: default @@ -533,6 +539,8 @@ Settings: ocsp: http://ocsp.example.org/ crl: http://crl.example.org/%s.crl issuer: http://%s.cas.example.org/ + serial: + device: /dev/ttyUSB0 KeyStorage: default: type: softhsm @@ -656,6 +664,8 @@ Settings: ocsp: http://ocsp.example.org/%s crl: http://crl.example.org/%s.crl issuer: http://%s.cas.example.org/ + serial: + device: /dev/ttyUSB0 KeyStorage: default: type: softhsm diff --git a/pkg/health/health.go b/pkg/health/health.go new file mode 100644 index 0000000..368f829 --- /dev/null +++ b/pkg/health/health.go @@ -0,0 +1,27 @@ +/* +Copyright 2022 CAcert Inc. +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package health implements Health checks +package health + +type Handler struct { + Version string +} + +func New(version string) *Handler { + return &Handler{Version: version} +} diff --git a/pkg/hsm/context_test.go b/pkg/hsm/context_test.go index 289f594..835e913 100644 --- a/pkg/hsm/context_test.go +++ b/pkg/hsm/context_test.go @@ -183,6 +183,8 @@ Settings: ocsp: http://ocsp.example.org/ crl: http://crl.example.org/%s.crl issuer: http://%s.cas.example.org/ + serial: + device: /dev/ttyUSB0 CAs: root: common-name: "Acme CAs root" diff --git a/pkg/hsm/hsm_test.go b/pkg/hsm/hsm_test.go index 777ea92..cc54c1d 100644 --- a/pkg/hsm/hsm_test.go +++ b/pkg/hsm/hsm_test.go @@ -157,6 +157,8 @@ Settings: ocsp: http://ocsp.example.org/ crl: http://crl.example.org/%s.crl issuer: http://%s.cas.example.org/ + serial: + device: /dev/ttyUSB0 CAs: root: common-name: "Acme CAs root" diff --git a/pkg/messages/messages.go b/pkg/messages/messages.go new file mode 100644 index 0000000..93e66be --- /dev/null +++ b/pkg/messages/messages.go @@ -0,0 +1,68 @@ +/* +Copyright 2022 CAcert Inc. +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +//go:generate go run github.com/shamaton/msgpackgen + +// Package messages contains structure definitions for protocol messages +package messages + +import ( + "fmt" + "time" +) + +type CommandCode int + +const ( + CmdHealth CommandCode = iota +) + +func (c CommandCode) String() string { + switch c { + case CmdHealth: + return "HEALTH" + default: + return fmt.Sprintf("unknown (%d)", int(c)) + } +} + +type Command struct { + Code CommandCode `msgpack:"code"` + TimeStamp time.Time `msgpack:"created"` + Payload interface{} `msgpack:"payload"` // optional payload +} + +type ResponseCode int + +const ( + RspError ResponseCode = -1 + RspHealth ResponseCode = iota +) + +type Response struct { + Code ResponseCode `msgpack:"code"` + TimeStamp time.Time `msgpack:"created"` + Payload interface{} `msgpack:"payload"` +} + +type HealthResponse struct { + Version string `msgpack:"version"` +} + +type ErrorResponse struct { + Message string `msgpack:"message"` +} diff --git a/pkg/messages/resolver.msgpackgen.go b/pkg/messages/resolver.msgpackgen.go new file mode 100644 index 0000000..1f34803 --- /dev/null +++ b/pkg/messages/resolver.msgpackgen.go @@ -0,0 +1,439 @@ +// Code generated by msgpackgen. DO NOT EDIT. + +package messages + +import ( + "fmt" + msgpack "github.com/shamaton/msgpackgen/msgpack" + dec "github.com/shamaton/msgpackgen/msgpack/dec" + enc "github.com/shamaton/msgpackgen/msgpack/enc" +) + +// RegisterGeneratedResolver registers generated resolver. +func RegisterGeneratedResolver() { + msgpack.SetResolver(___encodeAsMap, ___encodeAsArray, ___decodeAsMap, ___decodeAsArray) +} + +// encode +func ___encode(i interface{}) ([]byte, error) { + if msgpack.StructAsArray() { + return ___encodeAsArray(i) + } else { + return ___encodeAsMap(i) + } +} + +// encodeAsArray +func ___encodeAsArray(i interface{}) ([]byte, error) { + switch v := i.(type) { + case HealthResponse: + encoder := enc.NewEncoder() + size, err := ___calcArraySizeHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v, encoder) + if err != nil { + return nil, err + } + encoder.MakeBytes(size) + b, offset, err := ___encodeArrayHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v, encoder, 0) + if err != nil { + return nil, err + } + if size != offset { + return nil, fmt.Errorf("%s size / offset different %d : %d", "HealthResponse", size, offset) + } + return b, err + case *HealthResponse: + encoder := enc.NewEncoder() + size, err := ___calcArraySizeHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(*v, encoder) + if err != nil { + return nil, err + } + encoder.MakeBytes(size) + b, offset, err := ___encodeArrayHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(*v, encoder, 0) + if err != nil { + return nil, err + } + if size != offset { + return nil, fmt.Errorf("%s size / offset different %d : %d", "HealthResponse", size, offset) + } + return b, err + case ErrorResponse: + encoder := enc.NewEncoder() + size, err := ___calcArraySizeErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v, encoder) + if err != nil { + return nil, err + } + encoder.MakeBytes(size) + b, offset, err := ___encodeArrayErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v, encoder, 0) + if err != nil { + return nil, err + } + if size != offset { + return nil, fmt.Errorf("%s size / offset different %d : %d", "ErrorResponse", size, offset) + } + return b, err + case *ErrorResponse: + encoder := enc.NewEncoder() + size, err := ___calcArraySizeErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(*v, encoder) + if err != nil { + return nil, err + } + encoder.MakeBytes(size) + b, offset, err := ___encodeArrayErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(*v, encoder, 0) + if err != nil { + return nil, err + } + if size != offset { + return nil, fmt.Errorf("%s size / offset different %d : %d", "ErrorResponse", size, offset) + } + return b, err + } + return nil, nil +} + +// encodeAsMap +func ___encodeAsMap(i interface{}) ([]byte, error) { + switch v := i.(type) { + case HealthResponse: + encoder := enc.NewEncoder() + size, err := ___calcMapSizeHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v, encoder) + if err != nil { + return nil, err + } + encoder.MakeBytes(size) + b, offset, err := ___encodeMapHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v, encoder, 0) + if err != nil { + return nil, err + } + if size != offset { + return nil, fmt.Errorf("%s size / offset different %d : %d", "HealthResponse", size, offset) + } + return b, err + case *HealthResponse: + encoder := enc.NewEncoder() + size, err := ___calcMapSizeHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(*v, encoder) + if err != nil { + return nil, err + } + encoder.MakeBytes(size) + b, offset, err := ___encodeMapHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(*v, encoder, 0) + if err != nil { + return nil, err + } + if size != offset { + return nil, fmt.Errorf("%s size / offset different %d : %d", "HealthResponse", size, offset) + } + return b, err + case ErrorResponse: + encoder := enc.NewEncoder() + size, err := ___calcMapSizeErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v, encoder) + if err != nil { + return nil, err + } + encoder.MakeBytes(size) + b, offset, err := ___encodeMapErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v, encoder, 0) + if err != nil { + return nil, err + } + if size != offset { + return nil, fmt.Errorf("%s size / offset different %d : %d", "ErrorResponse", size, offset) + } + return b, err + case *ErrorResponse: + encoder := enc.NewEncoder() + size, err := ___calcMapSizeErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(*v, encoder) + if err != nil { + return nil, err + } + encoder.MakeBytes(size) + b, offset, err := ___encodeMapErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(*v, encoder, 0) + if err != nil { + return nil, err + } + if size != offset { + return nil, fmt.Errorf("%s size / offset different %d : %d", "ErrorResponse", size, offset) + } + return b, err + } + return nil, nil +} + +// decode +func ___decode(data []byte, i interface{}) (bool, error) { + if msgpack.StructAsArray() { + return ___decodeAsArray(data, i) + } else { + return ___decodeAsMap(data, i) + } +} + +// decodeAsArray +func ___decodeAsArray(data []byte, i interface{}) (bool, error) { + switch v := i.(type) { + case *HealthResponse: + decoder := dec.NewDecoder(data) + offset, err := ___decodeArrayHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v, decoder, 0) + if err == nil && offset != decoder.Len() { + return true, fmt.Errorf("read length is different [%d] [%d] ", offset, decoder.Len()) + } + return true, err + case **HealthResponse: + decoder := dec.NewDecoder(data) + offset, err := ___decodeArrayHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(*v, decoder, 0) + if err == nil && offset != decoder.Len() { + return true, fmt.Errorf("read length is different [%d] [%d] ", offset, decoder.Len()) + } + return true, err + case *ErrorResponse: + decoder := dec.NewDecoder(data) + offset, err := ___decodeArrayErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v, decoder, 0) + if err == nil && offset != decoder.Len() { + return true, fmt.Errorf("read length is different [%d] [%d] ", offset, decoder.Len()) + } + return true, err + case **ErrorResponse: + decoder := dec.NewDecoder(data) + offset, err := ___decodeArrayErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(*v, decoder, 0) + if err == nil && offset != decoder.Len() { + return true, fmt.Errorf("read length is different [%d] [%d] ", offset, decoder.Len()) + } + return true, err + } + return false, nil +} + +// decodeAsMap +func ___decodeAsMap(data []byte, i interface{}) (bool, error) { + switch v := i.(type) { + case *HealthResponse: + decoder := dec.NewDecoder(data) + offset, err := ___decodeMapHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v, decoder, 0) + if err == nil && offset != decoder.Len() { + return true, fmt.Errorf("read length is different [%d] [%d] ", offset, decoder.Len()) + } + return true, err + case **HealthResponse: + decoder := dec.NewDecoder(data) + offset, err := ___decodeMapHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(*v, decoder, 0) + if err == nil && offset != decoder.Len() { + return true, fmt.Errorf("read length is different [%d] [%d] ", offset, decoder.Len()) + } + return true, err + case *ErrorResponse: + decoder := dec.NewDecoder(data) + offset, err := ___decodeMapErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v, decoder, 0) + if err == nil && offset != decoder.Len() { + return true, fmt.Errorf("read length is different [%d] [%d] ", offset, decoder.Len()) + } + return true, err + case **ErrorResponse: + decoder := dec.NewDecoder(data) + offset, err := ___decodeMapErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(*v, decoder, 0) + if err == nil && offset != decoder.Len() { + return true, fmt.Errorf("read length is different [%d] [%d] ", offset, decoder.Len()) + } + return true, err + } + return false, nil +} + +// calculate size from git.cacert.org/cacert-gosigner/pkg/messages.HealthResponse +func ___calcArraySizeHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v HealthResponse, encoder *enc.Encoder) (int, error) { + size := 0 + size += encoder.CalcStructHeaderFix(1) + size += encoder.CalcString(v.Version) + return size, nil +} + +// calculate size from git.cacert.org/cacert-gosigner/pkg/messages.HealthResponse +func ___calcMapSizeHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v HealthResponse, encoder *enc.Encoder) (int, error) { + size := 0 + size += encoder.CalcStructHeaderFix(1) + size += encoder.CalcStringFix(7) + size += encoder.CalcString(v.Version) + return size, nil +} + +// encode from git.cacert.org/cacert-gosigner/pkg/messages.HealthResponse +func ___encodeArrayHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v HealthResponse, encoder *enc.Encoder, offset int) ([]byte, int, error) { + var err error + offset = encoder.WriteStructHeaderFixAsArray(1, offset) + offset = encoder.WriteString(v.Version, offset) + return encoder.EncodedBytes(), offset, err +} + +// encode from git.cacert.org/cacert-gosigner/pkg/messages.HealthResponse +func ___encodeMapHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v HealthResponse, encoder *enc.Encoder, offset int) ([]byte, int, error) { + var err error + offset = encoder.WriteStructHeaderFixAsMap(1, offset) + offset = encoder.WriteStringFix("version", 7, offset) + offset = encoder.WriteString(v.Version, offset) + return encoder.EncodedBytes(), offset, err +} + +// decode to git.cacert.org/cacert-gosigner/pkg/messages.HealthResponse +func ___decodeArrayHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v *HealthResponse, decoder *dec.Decoder, offset int) (int, error) { + offset, err := decoder.CheckStructHeader(1, offset) + if err != nil { + return 0, err + } + { + var vv string + vv, offset, err = decoder.AsString(offset) + if err != nil { + return 0, err + } + v.Version = vv + } + return offset, err +} + +// decode to git.cacert.org/cacert-gosigner/pkg/messages.HealthResponse +func ___decodeMapHealthResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v *HealthResponse, decoder *dec.Decoder, offset int) (int, error) { + keys := [][]byte{ + {uint8(0x76), uint8(0x65), uint8(0x72), uint8(0x73), uint8(0x69), uint8(0x6f), uint8(0x6e)}, // version + } + offset, err := decoder.CheckStructHeader(1, offset) + if err != nil { + return 0, err + } + count := 0 + for count < 1 { + var dataKey []byte + dataKey, offset, err = decoder.AsStringBytes(offset) + if err != nil { + return 0, err + } + fieldIndex := -1 + for i, key := range keys { + if len(dataKey) != len(key) { + continue + } + fieldIndex = i + for dataKeyIndex := range dataKey { + if dataKey[dataKeyIndex] != key[dataKeyIndex] { + fieldIndex = -1 + break + } + } + if fieldIndex >= 0 { + break + } + } + switch fieldIndex { + case 0: + { + var vv string + vv, offset, err = decoder.AsString(offset) + if err != nil { + return 0, err + } + v.Version = vv + } + count++ + default: + return 0, fmt.Errorf("unknown key[%s] found", string(dataKey)) + } + } + return offset, err +} + +// calculate size from git.cacert.org/cacert-gosigner/pkg/messages.ErrorResponse +func ___calcArraySizeErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v ErrorResponse, encoder *enc.Encoder) (int, error) { + size := 0 + size += encoder.CalcStructHeaderFix(1) + size += encoder.CalcString(v.Message) + return size, nil +} + +// calculate size from git.cacert.org/cacert-gosigner/pkg/messages.ErrorResponse +func ___calcMapSizeErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v ErrorResponse, encoder *enc.Encoder) (int, error) { + size := 0 + size += encoder.CalcStructHeaderFix(1) + size += encoder.CalcStringFix(7) + size += encoder.CalcString(v.Message) + return size, nil +} + +// encode from git.cacert.org/cacert-gosigner/pkg/messages.ErrorResponse +func ___encodeArrayErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v ErrorResponse, encoder *enc.Encoder, offset int) ([]byte, int, error) { + var err error + offset = encoder.WriteStructHeaderFixAsArray(1, offset) + offset = encoder.WriteString(v.Message, offset) + return encoder.EncodedBytes(), offset, err +} + +// encode from git.cacert.org/cacert-gosigner/pkg/messages.ErrorResponse +func ___encodeMapErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v ErrorResponse, encoder *enc.Encoder, offset int) ([]byte, int, error) { + var err error + offset = encoder.WriteStructHeaderFixAsMap(1, offset) + offset = encoder.WriteStringFix("message", 7, offset) + offset = encoder.WriteString(v.Message, offset) + return encoder.EncodedBytes(), offset, err +} + +// decode to git.cacert.org/cacert-gosigner/pkg/messages.ErrorResponse +func ___decodeArrayErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v *ErrorResponse, decoder *dec.Decoder, offset int) (int, error) { + offset, err := decoder.CheckStructHeader(1, offset) + if err != nil { + return 0, err + } + { + var vv string + vv, offset, err = decoder.AsString(offset) + if err != nil { + return 0, err + } + v.Message = vv + } + return offset, err +} + +// decode to git.cacert.org/cacert-gosigner/pkg/messages.ErrorResponse +func ___decodeMapErrorResponse_e587a81c7cb163b35488bdef0f58c292f99f4bd65a81377f81e5b18c3d86855d(v *ErrorResponse, decoder *dec.Decoder, offset int) (int, error) { + keys := [][]byte{ + {uint8(0x6d), uint8(0x65), uint8(0x73), uint8(0x73), uint8(0x61), uint8(0x67), uint8(0x65)}, // message + } + offset, err := decoder.CheckStructHeader(1, offset) + if err != nil { + return 0, err + } + count := 0 + for count < 1 { + var dataKey []byte + dataKey, offset, err = decoder.AsStringBytes(offset) + if err != nil { + return 0, err + } + fieldIndex := -1 + for i, key := range keys { + if len(dataKey) != len(key) { + continue + } + fieldIndex = i + for dataKeyIndex := range dataKey { + if dataKey[dataKeyIndex] != key[dataKeyIndex] { + fieldIndex = -1 + break + } + } + if fieldIndex >= 0 { + break + } + } + switch fieldIndex { + case 0: + { + var vv string + vv, offset, err = decoder.AsString(offset) + if err != nil { + return 0, err + } + v.Message = vv + } + count++ + default: + return 0, fmt.Errorf("unknown key[%s] found", string(dataKey)) + } + } + return offset, err +} diff --git a/pkg/protocol/protocol.go b/pkg/protocol/protocol.go new file mode 100644 index 0000000..086e45a --- /dev/null +++ b/pkg/protocol/protocol.go @@ -0,0 +1,129 @@ +/* +Copyright 2022 CAcert Inc. +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package protocol handles the protocol message marshaling and unmarshalling. +package protocol + +import ( + "fmt" + "log" + "time" + + "github.com/shamaton/msgpackgen/msgpack" + + "git.cacert.org/cacert-gosigner/pkg/health" + "git.cacert.org/cacert-gosigner/pkg/messages" +) + +// Handler is responsible for parsing incoming frames and calling commands +type Handler interface { + HandleFrame([]byte) ([]byte, error) +} + +type MsgPackHandler struct { + infoLog, errorLog *log.Logger + healthHandler *health.Handler +} + +func (m *MsgPackHandler) HandleFrame(frame []byte) ([]byte, error) { + var command messages.Command + + err := msgpack.Unmarshal(frame, &command) + if err != nil { + m.errorLog.Printf("unmarshal failed: %v", err) + + errorResponse, innerErr := buildErrorResponse("do not understand") + if innerErr != nil { + return nil, innerErr + } + + return errorResponse, nil + } + + m.infoLog.Printf("Received %s command sent at %s", command.Code, command.TimeStamp) + + response, err := m.handleCommand(&command) + if err != nil { + m.errorLog.Printf("command failed: %v", err) + + errorResponse, innerErr := buildErrorResponse("command failed") + if innerErr != nil { + return nil, innerErr + } + + return errorResponse, nil + } + + responseData, err := msgpack.Marshal(response) + if err != nil { + return nil, fmt.Errorf("could not marshal response: %w", err) + } + + return responseData, nil +} + +func (m *MsgPackHandler) handleCommand(command *messages.Command) (*messages.Response, error) { + var ( + payload interface{} + responseCode messages.ResponseCode + ) + + switch command.Code { + case messages.CmdHealth: + responseCode, payload = messages.RspHealth, messages.HealthResponse{Version: m.healthHandler.Version} + default: + return nil, fmt.Errorf("unhandled command %s", command) + } + + return &messages.Response{TimeStamp: time.Now().UTC(), Code: responseCode, Payload: payload}, nil +} + +func buildErrorResponse(errMsg string) ([]byte, error) { + marshal, err := msgpack.Marshal(&messages.Response{ + Code: messages.RspError, + TimeStamp: time.Now().UTC(), + Payload: messages.ErrorResponse{Message: errMsg}, + }) + if err != nil { + return nil, fmt.Errorf("could not marshal error response: %w", err) + } + + return marshal, nil +} + +func New(infoLog *log.Logger, errorLog *log.Logger, handlers ...RegisterHandler) (Handler, error) { + messages.RegisterGeneratedResolver() + + h := &MsgPackHandler{ + infoLog: infoLog, + errorLog: errorLog, + } + + for _, reg := range handlers { + reg(h) + } + + return h, nil +} + +type RegisterHandler func(handler *MsgPackHandler) + +func RegisterHealthHandler(healthHandler *health.Handler) func(*MsgPackHandler) { + return func(h *MsgPackHandler) { + h.healthHandler = healthHandler + } +} diff --git a/pkg/seriallink/seriallink.go b/pkg/seriallink/seriallink.go new file mode 100644 index 0000000..0dfc1ca --- /dev/null +++ b/pkg/seriallink/seriallink.go @@ -0,0 +1,134 @@ +/* +Copyright 2022 CAcert Inc. +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package seriallink provides a handler for the serial connection of the signer machine. +package seriallink + +import ( + "fmt" + "time" + + "github.com/justincpresley/go-cobs" + "github.com/tarm/serial" + + "git.cacert.org/cacert-gosigner/pkg/config" + "git.cacert.org/cacert-gosigner/pkg/protocol" +) + +type Handler struct { + protocolHandler protocol.Handler + config *serial.Config + port *serial.Port +} + +func (h *Handler) setupConnection() error { + s, err := serial.OpenPort(h.config) + if err != nil { + return fmt.Errorf("could not open serial port: %w", err) + } + + h.port = s + + return nil +} + +func (h *Handler) Close() error { + err := h.port.Close() + if err != nil { + return fmt.Errorf("could not close serial port: %w", err) + } + + return nil +} + +const cobsDelimiter = 0x00 + +func (h *Handler) Run() error { + const ( + bufferSize = 1024 * 1024 + readInterval = 50 * time.Millisecond + ) + + errors := make(chan error) + + cobsConfig := cobs.Config{SpecialByte: cobsDelimiter, Delimiter: true, EndingSave: true} + + go func() { + buf := make([]byte, bufferSize) + + for { + count, err := h.port.Read(buf) + if err != nil { + errors <- err + + return + } + + if count == 0 { + time.Sleep(readInterval) + + continue + } + + err = cobs.Verify(buf[:count], cobsConfig) + if err != nil { + errors <- err + + return + } + + // perform COBS decoding + decoded := cobs.Decode(buf[:count], cobsConfig) + + msg, err := h.protocolHandler.HandleFrame(decoded) + if err != nil { + errors <- err + + return + } + + // perform COBS encoding + encoded := cobs.Encode(msg, cobsConfig) + + _, err = h.port.Write(encoded) + if err != nil { + errors <- err + + return + } + } + }() + + err := <-errors + if err != nil { + return fmt.Errorf("error from handler loop: %w", err) + } + + return nil +} + +func New(cfg *config.Serial, protocolHandler protocol.Handler) (*Handler, error) { + h := &Handler{protocolHandler: protocolHandler} + h.config = &serial.Config{Name: cfg.Device, Baud: cfg.Baud, ReadTimeout: cfg.Timeout} + + err := h.setupConnection() + if err != nil { + return nil, err + } + + return h, nil +}