From 5949b497c5bb6e1b2bd722860e656c1e262b5a5c Mon Sep 17 00:00:00 2001 From: german2112 Date: Sat, 5 Oct 2024 03:52:46 +0800 Subject: [PATCH] german torres signing service challenge solution --- signing-service-challenge-go/api/device.go | 99 ++++++++++++++ signing-service-challenge-go/api/dto.go | 14 ++ signing-service-challenge-go/api/server.go | 26 +++- .../api/transaction.go | 55 ++++++++ signing-service-challenge-go/crypto/ecdsa.go | 15 +- .../crypto/ecdsa_generator.go | 28 ++++ signing-service-challenge-go/crypto/errors.go | 39 ++++++ .../crypto/generation.go | 42 ------ signing-service-challenge-go/crypto/rsa.go | 15 +- .../crypto/rsa_generator.go | 27 ++++ signing-service-challenge-go/crypto/signer.go | 50 +++++++ .../crypto/signer_test.go | 63 +++++++++ signing-service-challenge-go/domain/device.go | 55 ++++++++ signing-service-challenge-go/go.mod | 16 ++- .../helper/error_handler.go | 36 +++++ signing-service-challenge-go/main.go | 34 ++++- .../mocks/mock_device_repository.go | 33 +++++ .../mocks/mock_ecdsa_generator.go | 14 ++ .../mocks/mock_ecdsa_marshaler.go | 22 +++ .../mocks/mock_rsa_generator.go | 14 ++ .../mocks/mock_rsa_marshaler.go | 22 +++ .../mocks/mock_signer.go | 12 ++ .../persistence/inmemory.go | 50 +++++++ .../service/device_service.go | 88 ++++++++++++ .../service/device_service_test.go | 128 ++++++++++++++++++ .../service/errors.go | 57 ++++++++ .../service/transaction_service.go | 39 ++++++ .../service/transaction_service_test.go | 124 +++++++++++++++++ 28 files changed, 1148 insertions(+), 69 deletions(-) create mode 100644 signing-service-challenge-go/api/dto.go create mode 100644 signing-service-challenge-go/api/transaction.go create mode 100644 signing-service-challenge-go/crypto/ecdsa_generator.go create mode 100644 signing-service-challenge-go/crypto/errors.go delete mode 100644 signing-service-challenge-go/crypto/generation.go create mode 100644 signing-service-challenge-go/crypto/rsa_generator.go create mode 100644 signing-service-challenge-go/crypto/signer_test.go create mode 100644 signing-service-challenge-go/helper/error_handler.go create mode 100644 signing-service-challenge-go/mocks/mock_device_repository.go create mode 100644 signing-service-challenge-go/mocks/mock_ecdsa_generator.go create mode 100644 signing-service-challenge-go/mocks/mock_ecdsa_marshaler.go create mode 100644 signing-service-challenge-go/mocks/mock_rsa_generator.go create mode 100644 signing-service-challenge-go/mocks/mock_rsa_marshaler.go create mode 100644 signing-service-challenge-go/mocks/mock_signer.go create mode 100644 signing-service-challenge-go/service/device_service.go create mode 100644 signing-service-challenge-go/service/device_service_test.go create mode 100644 signing-service-challenge-go/service/errors.go create mode 100644 signing-service-challenge-go/service/transaction_service.go create mode 100644 signing-service-challenge-go/service/transaction_service_test.go diff --git a/signing-service-challenge-go/api/device.go b/signing-service-challenge-go/api/device.go index 74f7c07..d2fc3b5 100644 --- a/signing-service-challenge-go/api/device.go +++ b/signing-service-challenge-go/api/device.go @@ -1,3 +1,102 @@ package api +import ( + "encoding/json" + "net/http" + "signing-service-challenge/domain" + "signing-service-challenge/helper" + "signing-service-challenge/service" + + "github.com/gorilla/mux" +) + +type DeviceHandler struct { + deviceService service.DeviceService +} + +func NewDeviceHandler(deviceService service.DeviceService) *DeviceHandler { + return &DeviceHandler{deviceService: deviceService} +} + +type CreateSignatureDeviceRequest struct { + Algorithm string `json:"algorithm"` + Label string `json:"label"` +} + +type CreateSignatureDeviceResponse struct { + Algorithm string `json:"algorithm"` + Label string `json:"label"` +} + // TODO: REST endpoints ... +func (s *DeviceHandler) CreateSignatureDevice(w http.ResponseWriter, r *http.Request) { + var req CreateSignatureDeviceRequest + var device *domain.Device + w.Header().Set("Content-type", "application/json") + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + WriteErrorResponse(w, http.StatusBadRequest, []string{"Invalid Payload."}) + } + + if req.Algorithm == "" || req.Label == "" { + WriteErrorResponse(w, http.StatusBadRequest, []string{"Missing required field in payload: algorithm, label."}) + return + } + + device, err := s.deviceService.CreateDevice(req.Label, domain.AlgorithmType(req.Algorithm)) + if err != nil { + code, msg := helper.HandleDeviceServiceError(err) + WriteErrorResponse(w, code, []string{msg}) + return + } + + response := CreateSignatureDeviceResponse{ + Label: device.Label, + Algorithm: string(device.Algorithm), + } + + WriteAPIResponse(w, http.StatusCreated, response) + +} + +func (s *DeviceHandler) ListDevices(w http.ResponseWriter, r *http.Request) { + devices, err := s.deviceService.ListDevices() + if err != nil { + WriteErrorResponse(w, http.StatusInternalServerError, []string{"Failed to retrieve list of devices"}) + return + } + + response := make([]DeviceListResponse, len(devices)) + for i, device := range devices { + response[i] = DeviceListResponse{ + Id: device.Id, + Label: device.Label, + Algorithm: string(device.Algorithm), + } + } + WriteAPIResponse(w, http.StatusOK, response) +} + +func (s *DeviceHandler) GetDeviceById(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + deviceId := vars["deviceId"] + + if deviceId == "" { + WriteErrorResponse(w, http.StatusBadRequest, []string{"Missing required field in parameters: deviceId."}) + return + } + + device, err := s.deviceService.GetDeviceById(deviceId) + if err != nil { + code, msg := helper.HandleDeviceServiceError(err) + WriteErrorResponse(w, code, []string{msg}) + return + } + + response := DeviceByIdResponse{ + Id: device.Id, + Label: device.Label, + Algorithm: string(device.Algorithm), + SignatureCounter: device.SignatureCounter, + } + WriteAPIResponse(w, http.StatusOK, response) +} diff --git a/signing-service-challenge-go/api/dto.go b/signing-service-challenge-go/api/dto.go new file mode 100644 index 0000000..21d7664 --- /dev/null +++ b/signing-service-challenge-go/api/dto.go @@ -0,0 +1,14 @@ +package api + +type DeviceListResponse struct { + Id string `json:"id"` + Label string `json:"label"` + Algorithm string `json:"algorithm"` +} + +type DeviceByIdResponse struct { + Id string `json:"id"` + Label string `json:"label"` + Algorithm string `json:"algorithm"` + SignatureCounter int `json:"signature_counter"` +} diff --git a/signing-service-challenge-go/api/server.go b/signing-service-challenge-go/api/server.go index c0bb3a0..b88677a 100644 --- a/signing-service-challenge-go/api/server.go +++ b/signing-service-challenge-go/api/server.go @@ -3,6 +3,8 @@ package api import ( "encoding/json" "net/http" + + "github.com/gorilla/mux" ) // Response is the generic API response container. @@ -17,26 +19,38 @@ type ErrorResponse struct { // Server manages HTTP requests and dispatches them to the appropriate services. type Server struct { - listenAddress string + listenAddress string + deviceHandler *DeviceHandler + transactionHandler *TransactionHandler } // NewServer is a factory to instantiate a new Server. -func NewServer(listenAddress string) *Server { +func NewServer(listenAddress string, deviceServiceHanlder *DeviceHandler, transactionHandler *TransactionHandler) *Server { + return &Server{ - listenAddress: listenAddress, + listenAddress: listenAddress, + transactionHandler: transactionHandler, + deviceHandler: deviceServiceHanlder, // TODO: add services / further dependencies here ... } } // Run registers all HandlerFuncs for the existing HTTP routes and starts the Server. func (s *Server) Run() error { - mux := http.NewServeMux() + router := mux.NewRouter() - mux.Handle("/api/v0/health", http.HandlerFunc(s.Health)) + router.Handle("/api/v0/health", http.HandlerFunc(s.Health)).Methods("GET") // TODO: register further HandlerFuncs here ... + //Device Handler + router.Handle("/api/v0/devices/create-device", http.HandlerFunc(s.deviceHandler.CreateSignatureDevice)).Methods("POST") + router.Handle("/api/v0/devices/list-devices", http.HandlerFunc(s.deviceHandler.ListDevices)).Methods("GET") + router.Handle("/api/v0/devices/{deviceId}", http.HandlerFunc(s.deviceHandler.GetDeviceById)).Methods("GET") + + //Transaction Handler + router.Handle("/api/v0/transactions/{deviceId}/sign", http.HandlerFunc(s.transactionHandler.SignTransactionHandler)).Methods("POST") - return http.ListenAndServe(s.listenAddress, mux) + return http.ListenAndServe(s.listenAddress, router) } // WriteInternalError writes a default internal error message as an HTTP response. diff --git a/signing-service-challenge-go/api/transaction.go b/signing-service-challenge-go/api/transaction.go new file mode 100644 index 0000000..24678fc --- /dev/null +++ b/signing-service-challenge-go/api/transaction.go @@ -0,0 +1,55 @@ +package api + +import ( + "encoding/base64" + "encoding/json" + "net/http" + "signing-service-challenge/helper" + "signing-service-challenge/service" + + "github.com/gorilla/mux" +) + +type TransactionHandler struct { + transactionService service.TransactionService +} + +func NewTransactionHandler(transactionService service.TransactionService) *TransactionHandler { + return &TransactionHandler{transactionService: transactionService} +} + +type SignTransactionRequest struct { + Data string +} + +type SignTransacitonResponse struct { + Signature string + Signed_data string +} + +func (s *TransactionHandler) SignTransactionHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + var req SignTransactionRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + WriteErrorResponse(w, http.StatusBadRequest, []string{"Invalid Payload."}) + return + } + + if vars["deviceId"] == "" || req.Data == "" { + WriteErrorResponse(w, http.StatusBadRequest, []string{"Missing required field in payload: deviceId, data."}) + return + } + + signature, dataToBeSigned, err := s.transactionService.SignTransaction(vars["deviceId"], req.Data) + if err != nil { + code, msg := helper.HandleDeviceServiceError(err) + WriteErrorResponse(w, code, []string{msg}) + return + } + + response := SignTransacitonResponse{ + Signature: base64.RawStdEncoding.EncodeToString(signature), + Signed_data: string(dataToBeSigned), + } + WriteAPIResponse(w, http.StatusOK, response) +} diff --git a/signing-service-challenge-go/crypto/ecdsa.go b/signing-service-challenge-go/crypto/ecdsa.go index 0c66daf..9b097a1 100644 --- a/signing-service-challenge-go/crypto/ecdsa.go +++ b/signing-service-challenge-go/crypto/ecdsa.go @@ -6,6 +6,10 @@ import ( "encoding/pem" ) +type ECCMarshaler interface { + Encode(keyPair ECCKeyPair) ([]byte, []byte, error) +} + // ECCKeyPair is a DTO that holds ECC private and public keys. type ECCKeyPair struct { Public *ecdsa.PublicKey @@ -13,16 +17,11 @@ type ECCKeyPair struct { } // ECCMarshaler can encode and decode an ECC key pair. -type ECCMarshaler struct{} - -// NewECCMarshaler creates a new ECCMarshaler. -func NewECCMarshaler() ECCMarshaler { - return ECCMarshaler{} -} +type DefaultECCMarshaler struct{} // Encode takes an ECCKeyPair and encodes it to be written on disk. // It returns the public and the private key as a byte slice. -func (m ECCMarshaler) Encode(keyPair ECCKeyPair) ([]byte, []byte, error) { +func (m DefaultECCMarshaler) Encode(keyPair ECCKeyPair) ([]byte, []byte, error) { privateKeyBytes, err := x509.MarshalECPrivateKey(keyPair.Private) if err != nil { return nil, nil, err @@ -47,7 +46,7 @@ func (m ECCMarshaler) Encode(keyPair ECCKeyPair) ([]byte, []byte, error) { } // Decode assembles an ECCKeyPair from an encoded private key. -func (m ECCMarshaler) Decode(privateKeyBytes []byte) (*ECCKeyPair, error) { +func (m DefaultECCMarshaler) Decode(privateKeyBytes []byte) (*ECCKeyPair, error) { block, _ := pem.Decode(privateKeyBytes) privateKey, err := x509.ParseECPrivateKey(block.Bytes) if err != nil { diff --git a/signing-service-challenge-go/crypto/ecdsa_generator.go b/signing-service-challenge-go/crypto/ecdsa_generator.go new file mode 100644 index 0000000..199bbd1 --- /dev/null +++ b/signing-service-challenge-go/crypto/ecdsa_generator.go @@ -0,0 +1,28 @@ +package crypto + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" +) + +type ECCGenerator interface { + Generate() (*ECCKeyPair, error) +} + +// ECCGenerator generates an ECC key pair. +type DefaultECCGenerator struct{} + +// Generate generates a new ECCKeyPair. +func (g *DefaultECCGenerator) Generate() (*ECCKeyPair, error) { + // Security has been ignored for the sake of simplicity. + key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return nil, err + } + + return &ECCKeyPair{ + Public: &key.PublicKey, + Private: key, + }, nil +} diff --git a/signing-service-challenge-go/crypto/errors.go b/signing-service-challenge-go/crypto/errors.go new file mode 100644 index 0000000..1bd460c --- /dev/null +++ b/signing-service-challenge-go/crypto/errors.go @@ -0,0 +1,39 @@ +package crypto + +import "fmt" + +// SignOperationError definition +type SignOperationError struct { + Algorithm string + Err error +} + +func (e *SignOperationError) Error() string { + return fmt.Sprintf("error while signing the data with algorithm %s: %s", e.Algorithm, e.Err) +} + +func (e *SignOperationError) Unwrap() error { //Unwrap included for logging purposes + return e.Err +} + +func NewSignOperationError(algorithm string, err error) *SignOperationError { + return &SignOperationError{Algorithm: algorithm, Err: err} +} + +// MarshalError definition +type MarshalError struct { + Algorithm string + Err error +} + +func (e *MarshalError) Error() string { + return fmt.Sprintf("error while marshaling the data with the algorithm %s: %s", e.Algorithm, e.Err) +} + +func (e *MarshalError) Unwrap() error { //Unwrap included for logging purposes + return e.Err +} + +func NewMarshalError(algorithm string, err error) *MarshalError { + return &MarshalError{Algorithm: algorithm, Err: err} +} diff --git a/signing-service-challenge-go/crypto/generation.go b/signing-service-challenge-go/crypto/generation.go deleted file mode 100644 index 0d8a0fc..0000000 --- a/signing-service-challenge-go/crypto/generation.go +++ /dev/null @@ -1,42 +0,0 @@ -package crypto - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" -) - -// RSAGenerator generates a RSA key pair. -type RSAGenerator struct{} - -// Generate generates a new RSAKeyPair. -func (g *RSAGenerator) Generate() (*RSAKeyPair, error) { - // Security has been ignored for the sake of simplicity. - key, err := rsa.GenerateKey(rand.Reader, 512) - if err != nil { - return nil, err - } - - return &RSAKeyPair{ - Public: &key.PublicKey, - Private: key, - }, nil -} - -// ECCGenerator generates an ECC key pair. -type ECCGenerator struct{} - -// Generate generates a new ECCKeyPair. -func (g *ECCGenerator) Generate() (*ECCKeyPair, error) { - // Security has been ignored for the sake of simplicity. - key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) - if err != nil { - return nil, err - } - - return &ECCKeyPair{ - Public: &key.PublicKey, - Private: key, - }, nil -} diff --git a/signing-service-challenge-go/crypto/rsa.go b/signing-service-challenge-go/crypto/rsa.go index 2a13bd0..668e8f8 100644 --- a/signing-service-challenge-go/crypto/rsa.go +++ b/signing-service-challenge-go/crypto/rsa.go @@ -6,6 +6,10 @@ import ( "encoding/pem" ) +type RSAMarshaler interface { + Marshal(keyPair RSAKeyPair) ([]byte, []byte, error) +} + // RSAKeyPair is a DTO that holds RSA private and public keys. type RSAKeyPair struct { Public *rsa.PublicKey @@ -13,16 +17,11 @@ type RSAKeyPair struct { } // RSAMarshaler can encode and decode an RSA key pair. -type RSAMarshaler struct{} - -// NewRSAMarshaler creates a new RSAMarshaler. -func NewRSAMarshaler() RSAMarshaler { - return RSAMarshaler{} -} +type DefaultRSAMarshaler struct{} // Marshal takes an RSAKeyPair and encodes it to be written on disk. // It returns the public and the private key as a byte slice. -func (m *RSAMarshaler) Marshal(keyPair RSAKeyPair) ([]byte, []byte, error) { +func (m *DefaultRSAMarshaler) Marshal(keyPair RSAKeyPair) ([]byte, []byte, error) { privateKeyBytes := x509.MarshalPKCS1PrivateKey(keyPair.Private) publicKeyBytes := x509.MarshalPKCS1PublicKey(keyPair.Public) @@ -40,7 +39,7 @@ func (m *RSAMarshaler) Marshal(keyPair RSAKeyPair) ([]byte, []byte, error) { } // Unmarshal takes an encoded RSA private key and transforms it into a rsa.PrivateKey. -func (m *RSAMarshaler) Unmarshal(privateKeyBytes []byte) (*RSAKeyPair, error) { +func (m *DefaultRSAMarshaler) Unmarshal(privateKeyBytes []byte) (*RSAKeyPair, error) { block, _ := pem.Decode(privateKeyBytes) privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { diff --git a/signing-service-challenge-go/crypto/rsa_generator.go b/signing-service-challenge-go/crypto/rsa_generator.go new file mode 100644 index 0000000..4e65b8f --- /dev/null +++ b/signing-service-challenge-go/crypto/rsa_generator.go @@ -0,0 +1,27 @@ +package crypto + +import ( + "crypto/rand" + "crypto/rsa" +) + +type RSAGenerator interface { + Generate() (*RSAKeyPair, error) +} + +// RSAGenerator generates a RSA key pair. +type DefaultRSAGenerator struct{} + +// Generate generates a new RSAKeyPair. +func (g *DefaultRSAGenerator) Generate() (*RSAKeyPair, error) { + // Security has been ignored for the sake of simplicity. + key, err := rsa.GenerateKey(rand.Reader, 512) + if err != nil { + return nil, err + } + + return &RSAKeyPair{ + Public: &key.PublicKey, + Private: key, + }, nil +} diff --git a/signing-service-challenge-go/crypto/signer.go b/signing-service-challenge-go/crypto/signer.go index 2b45414..829ea39 100644 --- a/signing-service-challenge-go/crypto/signer.go +++ b/signing-service-challenge-go/crypto/signer.go @@ -1,8 +1,58 @@ package crypto +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/asn1" + "math/big" +) + // Signer defines a contract for different types of signing implementations. type Signer interface { Sign(dataToBeSigned []byte) ([]byte, error) } +type ECCSigner struct { + privateKey *ecdsa.PrivateKey +} + +func NewECCSigner(privateKey *ecdsa.PrivateKey) *ECCSigner { + return &ECCSigner{privateKey: privateKey} +} + // TODO: implement RSA and ECDSA signing ... +func (s *ECCSigner) Sign(data []byte) ([]byte, error) { + hashedData := sha256.Sum256(data) + r, sValue, err := ecdsa.Sign(rand.Reader, s.privateKey, hashedData[:]) + if err != nil { + return nil, NewSignOperationError("ECDSA", err) + } + + signedData, err := asn1.Marshal(struct{ R, S *big.Int }{R: r, S: sValue}) + if err != nil { + return nil, NewMarshalError("ECDSA", err) + } + + return signedData, nil +} + +type RSASigner struct { + privateKey *rsa.PrivateKey +} + +func NewRSASigner(privateKey *rsa.PrivateKey) *RSASigner { + return &RSASigner{privateKey: privateKey} +} + +func (s *RSASigner) Sign(data []byte) ([]byte, error) { + hashedData := sha256.Sum256(data) + signedData, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, crypto.SHA256, hashedData[:]) + if err != nil { + return nil, NewSignOperationError("RSA", err) + } + + return signedData, nil +} diff --git a/signing-service-challenge-go/crypto/signer_test.go b/signing-service-challenge-go/crypto/signer_test.go new file mode 100644 index 0000000..2a21c41 --- /dev/null +++ b/signing-service-challenge-go/crypto/signer_test.go @@ -0,0 +1,63 @@ +package crypto + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewECCSigner(t *testing.T) { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.NoError(t, err) + assert.NotNil(t, privateKey) + + signer := NewECCSigner(privateKey) + + assert.NotNil(t, signer) + assert.Equal(t, privateKey, signer.privateKey) +} + +func TestECCSignerWithSignSuccess(t *testing.T) { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + assert.NoError(t, err) + + signer := NewECCSigner(privateKey) + + dataToBeSigned := []byte("sign-test-data") + + signedData, err := signer.Sign(dataToBeSigned) + + assert.NoError(t, err) + assert.NotNil(t, signedData) + assert.Greater(t, len(signedData), 0, "sign should not be empty") +} + +func TestNewRSASigner(t *testing.T) { + privateKey, err := rsa.GenerateKey(rand.Reader, 512) + assert.NoError(t, err) + assert.NotNil(t, privateKey) + + signer := NewRSASigner(privateKey) + + assert.NotNil(t, signer) + assert.Equal(t, privateKey, signer.privateKey) +} + +func TestRSASignerWithSignSuccess(t *testing.T) { + privateKey, err := rsa.GenerateKey(rand.Reader, 512) + assert.NoError(t, err) + + signer := NewRSASigner(privateKey) + + dataToBeSigned := []byte("sign-test-data") + + signedData, err := signer.Sign(dataToBeSigned) + + assert.NoError(t, err, "expect no error when signing data") + assert.NotNil(t, signedData, "expect signed data to not be nil") + assert.Greater(t, len(signedData), 0, "expect signed data to not be empty") +} diff --git a/signing-service-challenge-go/domain/device.go b/signing-service-challenge-go/domain/device.go index 504c7be..97dae26 100644 --- a/signing-service-challenge-go/domain/device.go +++ b/signing-service-challenge-go/domain/device.go @@ -1,3 +1,58 @@ package domain +import ( + "encoding/base64" + "fmt" + "signing-service-challenge/crypto" + "sync" +) + +type AlgorithmType string + +const ( + RSAAlgorithm AlgorithmType = "RSA" + ECCAlgorithm AlgorithmType = "ECC" +) + // TODO: signature device domain model ... +type Device struct { + mu sync.RWMutex + Id string + Label string + SignatureCounter int + Algorithm AlgorithmType + Signer crypto.Signer + LastSignature []byte + PublicKey []byte + PrivateKey []byte +} + +func (device *Device) GetSignatureReference() []byte { + device.mu.RLock() + defer device.mu.RUnlock() + if device.SignatureCounter == 0 { + return []byte(device.Id) + } + return device.LastSignature +} + +func (device *Device) BuildSecuredDataToBeSigned(data string) string { + signatureReference := device.GetSignatureReference() + encodedSignature := base64.StdEncoding.EncodeToString(signatureReference) + device.mu.RLock() + securedDataToBeSigned := fmt.Sprintf("%d_%s_%s", device.SignatureCounter, data, encodedSignature) + device.mu.RUnlock() + return securedDataToBeSigned +} + +func (device *Device) IncrementSignatureCounter() { + device.mu.Lock() + defer device.mu.Unlock() + device.SignatureCounter++ +} + +func (devide *Device) UpdateLastSignature(signature []byte) { + devide.mu.Lock() + defer devide.mu.Unlock() + devide.LastSignature = signature +} diff --git a/signing-service-challenge-go/go.mod b/signing-service-challenge-go/go.mod index 755af1a..f744a48 100644 --- a/signing-service-challenge-go/go.mod +++ b/signing-service-challenge-go/go.mod @@ -1,3 +1,17 @@ -module github.com/fiskaly/coding-challenges/signing-service-challenge +module signing-service-challenge go 1.20 + +require github.com/gorilla/mux v1.8.1 + +require ( + github.com/google/uuid v1.6.0 + github.com/stretchr/testify v1.9.0 +) + +// indirect dependencies +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/signing-service-challenge-go/helper/error_handler.go b/signing-service-challenge-go/helper/error_handler.go new file mode 100644 index 0000000..434155b --- /dev/null +++ b/signing-service-challenge-go/helper/error_handler.go @@ -0,0 +1,36 @@ +package helper + +import ( + "errors" + "net/http" + "signing-service-challenge/crypto" + "signing-service-challenge/service" +) + +func HandleDeviceServiceError(err error) (int, string) { + var keysGenerationError *service.KeysGenerationError + var keysEncodingError *service.KeysEncodingError + var invalidAlgorithmError *service.InvalidAlgorithmError + var deviceNotFoundError *service.DeviceNotFoundError + var signOperationError *crypto.SignOperationError + var marshalError *crypto.MarshalError + + switch { + case errors.As(err, &keysGenerationError): + return http.StatusUnprocessableEntity, keysGenerationError.Error() + case errors.As(err, &keysEncodingError): + return http.StatusUnprocessableEntity, keysEncodingError.Error() + case errors.As(err, &invalidAlgorithmError): + return http.StatusUnprocessableEntity, invalidAlgorithmError.Error() + case errors.As(err, &deviceNotFoundError): + return http.StatusNotFound, deviceNotFoundError.Error() + case errors.As(err, &deviceNotFoundError): + return http.StatusBadRequest, deviceNotFoundError.Error() + case errors.As(err, &signOperationError): + return http.StatusUnprocessableEntity, signOperationError.Error() + case errors.As(err, &marshalError): + return http.StatusUnprocessableEntity, marshalError.Error() + default: + return http.StatusInternalServerError, "Internal server error" + } +} diff --git a/signing-service-challenge-go/main.go b/signing-service-challenge-go/main.go index dce7bc8..200d782 100644 --- a/signing-service-challenge-go/main.go +++ b/signing-service-challenge-go/main.go @@ -2,17 +2,43 @@ package main import ( "log" - - "github.com/fiskaly/coding-challenges/signing-service-challenge/api" + "signing-service-challenge/api" + "signing-service-challenge/crypto" + "signing-service-challenge/persistence" + "signing-service-challenge/service" ) const ( - ListenAddress = ":8080" + ListenAddress = ":8081" // TODO: add further configuration parameters here ... ) func main() { - server := api.NewServer(ListenAddress) + + //initialize Inmemory + inmemoryDeviceRepository := persistence.NewInmemoryDeviceRepository() + // initialize Generators + eccGenerator := &crypto.DefaultECCGenerator{} + rsaGenerator := &crypto.DefaultRSAGenerator{} + + // initialize Marshalers + rsaMarshaler := &crypto.DefaultRSAMarshaler{} + ecdsaMarshaler := &crypto.DefaultECCMarshaler{} + + //Initialize Services + deviceService := service.NewDefaultDeviceService(inmemoryDeviceRepository, + eccGenerator, + rsaGenerator, + ecdsaMarshaler, + rsaMarshaler) + transactionService := service.NewDefaultTransactionService(inmemoryDeviceRepository) + + //Initialize Handlers + deviceHandler := api.NewDeviceHandler(deviceService) + transactionHandler := api.NewTransactionHandler(transactionService) + + //Initialize Server + server := api.NewServer(ListenAddress, deviceHandler, transactionHandler) if err := server.Run(); err != nil { log.Fatal("Could not start server on ", ListenAddress) diff --git a/signing-service-challenge-go/mocks/mock_device_repository.go b/signing-service-challenge-go/mocks/mock_device_repository.go new file mode 100644 index 0000000..954a9cb --- /dev/null +++ b/signing-service-challenge-go/mocks/mock_device_repository.go @@ -0,0 +1,33 @@ +package mocks + +import "signing-service-challenge/domain" + +type MockDeviceRepository struct { + UpdateDeviceCallsCount int + GetDeviceByIdCallsCount int + ListDevicesCallsCount int + DeviceToUpdate *domain.Device + DeviceToReturn *domain.Device + DevicesListToReturn []*domain.Device + GetDeviceByIdArg string + GetDeviceByIdFound bool +} + +func NewMockDeviceRepository() *MockDeviceRepository { + return &MockDeviceRepository{} +} + +func (m *MockDeviceRepository) UpdateDevice(device *domain.Device) { + m.UpdateDeviceCallsCount++ + m.DeviceToUpdate = device +} + +func (m *MockDeviceRepository) GetDeviceById(deviceId string) (*domain.Device, bool) { + m.GetDeviceByIdCallsCount++ + m.GetDeviceByIdArg = deviceId + return m.DeviceToReturn, m.GetDeviceByIdFound +} + +func (*MockDeviceRepository) ListDevices() ([]*domain.Device, error) { + return nil, nil +} diff --git a/signing-service-challenge-go/mocks/mock_ecdsa_generator.go b/signing-service-challenge-go/mocks/mock_ecdsa_generator.go new file mode 100644 index 0000000..4931a38 --- /dev/null +++ b/signing-service-challenge-go/mocks/mock_ecdsa_generator.go @@ -0,0 +1,14 @@ +package mocks + +import "signing-service-challenge/crypto" + +type MockECCGenerator struct { + GenerateFunc func() (*crypto.ECCKeyPair, error) +} + +func (m *MockECCGenerator) Generate() (*crypto.ECCKeyPair, error) { + if m.GenerateFunc != nil { + return m.GenerateFunc() + } + return nil, nil +} diff --git a/signing-service-challenge-go/mocks/mock_ecdsa_marshaler.go b/signing-service-challenge-go/mocks/mock_ecdsa_marshaler.go new file mode 100644 index 0000000..a685196 --- /dev/null +++ b/signing-service-challenge-go/mocks/mock_ecdsa_marshaler.go @@ -0,0 +1,22 @@ +package mocks + +import "signing-service-challenge/crypto" + +type MockECCMarshaler struct { + EncodeFunc func(keyPair crypto.ECCKeyPair) ([]byte, []byte, error) + DecodeFunc func(privateKeyBytes []byte) (*crypto.ECCKeyPair, error) +} + +func (m *MockECCMarshaler) Encode(keyPair crypto.ECCKeyPair) ([]byte, []byte, error) { + if m.EncodeFunc != nil { + return m.EncodeFunc(keyPair) + } + return nil, nil, nil +} + +func (m *MockECCMarshaler) Decode(privateKeyBytes []byte) (*crypto.ECCKeyPair, error) { + if m.DecodeFunc != nil { + return m.DecodeFunc(privateKeyBytes) + } + return nil, nil +} diff --git a/signing-service-challenge-go/mocks/mock_rsa_generator.go b/signing-service-challenge-go/mocks/mock_rsa_generator.go new file mode 100644 index 0000000..ae0392d --- /dev/null +++ b/signing-service-challenge-go/mocks/mock_rsa_generator.go @@ -0,0 +1,14 @@ +package mocks + +import "signing-service-challenge/crypto" + +type MockRSAGenerator struct { + GenerateFunc func() (*crypto.RSAKeyPair, error) +} + +func (m *MockRSAGenerator) Generate() (*crypto.RSAKeyPair, error) { + if m.GenerateFunc != nil { + return m.GenerateFunc() + } + return nil, nil +} diff --git a/signing-service-challenge-go/mocks/mock_rsa_marshaler.go b/signing-service-challenge-go/mocks/mock_rsa_marshaler.go new file mode 100644 index 0000000..63be2c0 --- /dev/null +++ b/signing-service-challenge-go/mocks/mock_rsa_marshaler.go @@ -0,0 +1,22 @@ +package mocks + +import "signing-service-challenge/crypto" + +type MockRSAMarshaler struct { + MarshalFunc func(keyPair crypto.RSAKeyPair) ([]byte, []byte, error) + UnmarshalFunc func(privateKeyBytes []byte) (*crypto.RSAKeyPair, error) +} + +func (m *MockRSAMarshaler) Marshal(keyPair crypto.RSAKeyPair) ([]byte, []byte, error) { + if m.MarshalFunc != nil { + return m.MarshalFunc(keyPair) + } + return nil, nil, nil +} + +func (m *MockRSAMarshaler) Unmarshal(privateKeyBytes []byte) (*crypto.RSAKeyPair, error) { + if m.UnmarshalFunc != nil { + return m.UnmarshalFunc(privateKeyBytes) + } + return nil, nil +} diff --git a/signing-service-challenge-go/mocks/mock_signer.go b/signing-service-challenge-go/mocks/mock_signer.go new file mode 100644 index 0000000..bff033f --- /dev/null +++ b/signing-service-challenge-go/mocks/mock_signer.go @@ -0,0 +1,12 @@ +package mocks + +type MockSigner struct { + SignFunc func(dataToBeSigned []byte) ([]byte, error) +} + +func (m *MockSigner) Sign(dataToBeSigned []byte) ([]byte, error) { + if m.SignFunc != nil { + return m.SignFunc(dataToBeSigned) + } + return nil, nil +} diff --git a/signing-service-challenge-go/persistence/inmemory.go b/signing-service-challenge-go/persistence/inmemory.go index a9e7094..7b49a47 100644 --- a/signing-service-challenge-go/persistence/inmemory.go +++ b/signing-service-challenge-go/persistence/inmemory.go @@ -1,3 +1,53 @@ package persistence +import ( + "signing-service-challenge/domain" + "sync" +) + +type DeviceRepository interface { + GetDeviceById(deviceId string) (*domain.Device, bool) + UpdateDevice(Device *domain.Device) + ListDevices() ([]*domain.Device, error) +} + // TODO: in-memory persistence ... +type InmemoryDeviceRepository struct { + mu *sync.RWMutex + Devices map[string]*domain.Device +} + +func NewInmemoryDeviceRepository() *InmemoryDeviceRepository { + return &InmemoryDeviceRepository{ + mu: &sync.RWMutex{}, + Devices: make(map[string]*domain.Device), + } +} + +func (r *InmemoryDeviceRepository) GetDeviceById(deviceId string) (*domain.Device, bool) { + r.mu.RLock() + defer r.mu.RUnlock() + Device, found := r.Devices[deviceId] + if !found { + return nil, false + } + + return Device, true +} + +func (r *InmemoryDeviceRepository) UpdateDevice(Device *domain.Device) { //Will keep update as upsert operation for the inmemory implementation + r.mu.Lock() + defer r.mu.Unlock() + r.Devices[Device.Id] = Device +} + +func (r *InmemoryDeviceRepository) ListDevices() ([]*domain.Device, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + var devices []*domain.Device + for _, device := range r.Devices { + devices = append(devices, device) + } + return devices, nil +} diff --git a/signing-service-challenge-go/service/device_service.go b/signing-service-challenge-go/service/device_service.go new file mode 100644 index 0000000..44cbf22 --- /dev/null +++ b/signing-service-challenge-go/service/device_service.go @@ -0,0 +1,88 @@ +package service + +import ( + "signing-service-challenge/crypto" + "signing-service-challenge/domain" + "signing-service-challenge/persistence" + + "github.com/google/uuid" +) + +type DeviceService interface { + CreateDevice(label string, algorithmType domain.AlgorithmType) (*domain.Device, error) + ListDevices() ([]*domain.Device, error) + GetDeviceById(deviceId string) (*domain.Device, error) +} + +type DefaultDeviceService struct { + deviceRepository persistence.DeviceRepository + rsaGenerator crypto.RSAGenerator + eccGenerator crypto.ECCGenerator + rsaMarshaler crypto.RSAMarshaler + ecdsaMarshaler crypto.ECCMarshaler +} + +func NewDefaultDeviceService(deviceRepository persistence.DeviceRepository, + eccGenerator crypto.ECCGenerator, + rsaGenerator crypto.RSAGenerator, + eccMarshaler crypto.ECCMarshaler, + rsaMarshaler crypto.RSAMarshaler) *DefaultDeviceService { + return &DefaultDeviceService{deviceRepository: deviceRepository, eccGenerator: eccGenerator, rsaGenerator: rsaGenerator, ecdsaMarshaler: eccMarshaler, rsaMarshaler: rsaMarshaler} +} + +func (s *DefaultDeviceService) CreateDevice(label string, algorithmType domain.AlgorithmType) (*domain.Device, error) { + id := uuid.NewString() + var publicKey, privateKey []byte + var signer crypto.Signer + switch algorithmType { + case domain.RSAAlgorithm: + rsakey, err := s.rsaGenerator.Generate() + if err != nil { + return nil, NewKeysGenerationError(string(algorithmType), err) + } + + signer = crypto.NewRSASigner(rsakey.Private) + publicKey, privateKey, err = s.rsaMarshaler.Marshal(*rsakey) + if err != nil { + return nil, NewKeysEncodingError(string(algorithmType), err) + } + + case domain.ECCAlgorithm: + eccKey, err := s.eccGenerator.Generate() + if err != nil { + return nil, NewKeysGenerationError(string(algorithmType), err) + } + + signer = crypto.NewECCSigner(eccKey.Private) + publicKey, privateKey, err = s.ecdsaMarshaler.Encode(*eccKey) + if err != nil { + return nil, NewKeysEncodingError(string(algorithmType), err) + } + default: + return nil, NewInvalidAlgorithmError(string(algorithmType)) + } + + createdDevice := &domain.Device{ + Id: id, + PublicKey: publicKey, + PrivateKey: privateKey, + SignatureCounter: 0, + Algorithm: algorithmType, + Signer: signer, + Label: label, + } + s.deviceRepository.UpdateDevice(createdDevice) + return createdDevice, nil +} + +func (s *DefaultDeviceService) GetDeviceById(deviceId string) (*domain.Device, error) { + device, found := s.deviceRepository.GetDeviceById(deviceId) + if !found { + return nil, NewDeviceNotFoundError(deviceId) + } + return device, nil +} + +func (s *DefaultDeviceService) ListDevices() ([]*domain.Device, error) { + return s.deviceRepository.ListDevices() +} diff --git a/signing-service-challenge-go/service/device_service_test.go b/signing-service-challenge-go/service/device_service_test.go new file mode 100644 index 0000000..c93bfc8 --- /dev/null +++ b/signing-service-challenge-go/service/device_service_test.go @@ -0,0 +1,128 @@ +package service + +import ( + "errors" + "signing-service-challenge/crypto" + "signing-service-challenge/domain" + "signing-service-challenge/mocks" + "signing-service-challenge/persistence" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCreateDeviceWithRSASuccess(t *testing.T) { + + mockRSAGenerator := &mocks.MockRSAGenerator{ + GenerateFunc: func() (*crypto.RSAKeyPair, error) { + return &crypto.RSAKeyPair{}, nil + }, + } + + mockRSAMarshaler := &mocks.MockRSAMarshaler{ + MarshalFunc: func(keyPair crypto.RSAKeyPair) ([]byte, []byte, error) { + return []byte("publicKey"), []byte("privateKey"), nil + }, + } + + mockDeviceRepository := mocks.NewMockDeviceRepository() + + deviceService := NewDefaultDeviceService(mockDeviceRepository, nil, mockRSAGenerator, nil, mockRSAMarshaler) + + device, err := deviceService.CreateDevice("rsa-sign-test", domain.RSAAlgorithm) + + assert.NoError(t, err, "expect no error when successfully created device") + assert.NotNil(t, device, "expect created device to be not nil") + assert.GreaterOrEqual(t, mockDeviceRepository.UpdateDeviceCallsCount, 1, "expect device to be updated in the repository") + assert.Equal(t, "rsa-sign-test", device.Label, "expect device label to be rsa-sign-test") + assert.Equal(t, domain.RSAAlgorithm, device.Algorithm, "expect device algorithm to be RSA") + assert.Equal(t, []byte("publicKey"), device.PublicKey, "expect device public key to be publicKey") + assert.Equal(t, []byte("privateKey"), device.PrivateKey, "expect device private key to be privateKey") +} + +func TestCreateDeviceWithECCSuccess(t *testing.T) { + + mockECCGenerator := &mocks.MockECCGenerator{ + GenerateFunc: func() (*crypto.ECCKeyPair, error) { + return &crypto.ECCKeyPair{}, nil + }, + } + + mockECCMarshaler := &mocks.MockECCMarshaler{ + EncodeFunc: func(keyPair crypto.ECCKeyPair) ([]byte, []byte, error) { + return []byte("publicKey"), []byte("privateKey"), nil + }, + } + + mockDeviceRepository := mocks.NewMockDeviceRepository() + + deviceService := NewDefaultDeviceService(mockDeviceRepository, mockECCGenerator, nil, mockECCMarshaler, nil) + + device, err := deviceService.CreateDevice("ecdsa-sign-test", domain.ECCAlgorithm) + + assert.NoError(t, err, "expect no error when successfully created device") + assert.NotNil(t, device, "expect created device to be not nil") + assert.GreaterOrEqual(t, mockDeviceRepository.UpdateDeviceCallsCount, 1, "expect device to be updated in the repository") + assert.Equal(t, "ecdsa-sign-test", device.Label, "expect device label to be ecdsa-sign-test") + assert.Equal(t, domain.ECCAlgorithm, device.Algorithm, "expect device algorithm to be ECC") + assert.Equal(t, []byte("publicKey"), device.PublicKey, "expect device public key to be publicKey") + assert.Equal(t, []byte("privateKey"), device.PrivateKey, "expect device private key to be privateKey") +} + +func TestCreateDeviceWithEncodingError(t *testing.T) { + mockRSAGenerator := &mocks.MockRSAGenerator{ + GenerateFunc: func() (*crypto.RSAKeyPair, error) { + return &crypto.RSAKeyPair{}, nil + }, + } + + mockRSAMarshaler := &mocks.MockRSAMarshaler{ + MarshalFunc: func(keyPair crypto.RSAKeyPair) ([]byte, []byte, error) { + return nil, nil, errors.New("error while encoding using rsa") + }, + } + + mockDeviceRepository := persistence.NewInmemoryDeviceRepository() + + deviceService := NewDefaultDeviceService(mockDeviceRepository, nil, mockRSAGenerator, nil, mockRSAMarshaler) + + device, err := deviceService.CreateDevice("rsa-sign-test", domain.RSAAlgorithm) + + assert.Nil(t, device, "expect device to be nil when error occurs") + assert.Error(t, err, "expect error when encoding error occurs") + + var encodingError *KeysEncodingError + isKeysEncodingError := errors.As(err, &encodingError) + assert.True(t, isKeysEncodingError, "expect error to be of type KeysEncodingError") + + if isKeysEncodingError { + assert.Equal(t, "RSA", encodingError.Algorithm, "expect algorithm to be RSA") + assert.EqualError(t, encodingError.Err, "error while encoding using rsa", "expect correct error message") + } +} + +func TestCreateDeviceWithKeysGenerationError(t *testing.T) { + mockRSAGenerator := &mocks.MockRSAGenerator{ + GenerateFunc: func() (*crypto.RSAKeyPair, error) { + return nil, errors.New("error while generating keys using rsa") + }, + } + + mockDeviceRepository := persistence.NewInmemoryDeviceRepository() + + deviceService := NewDefaultDeviceService(mockDeviceRepository, nil, mockRSAGenerator, nil, nil) + + device, err := deviceService.CreateDevice("rsa-sign-test", domain.RSAAlgorithm) + + assert.Nil(t, device, "expect device to be nil when error occurs") + assert.Error(t, err, "expect error when keys generation error occurs") + + var encodingError *KeysGenerationError + isKeysGenerationError := errors.As(err, &encodingError) + assert.True(t, isKeysGenerationError, "expect error to be of type KeysGenerationError") + + if isKeysGenerationError { + assert.Equal(t, "RSA", encodingError.Algorithm) + assert.EqualError(t, encodingError.Err, "error while generating keys using rsa", "expect correct error message") + } +} diff --git a/signing-service-challenge-go/service/errors.go b/signing-service-challenge-go/service/errors.go new file mode 100644 index 0000000..dda0d68 --- /dev/null +++ b/signing-service-challenge-go/service/errors.go @@ -0,0 +1,57 @@ +package service + +import "fmt" + +// InvalidAlgorithmError definition +type InvalidAlgorithmError struct { + Algorithm string +} + +func (e *InvalidAlgorithmError) Error() string { + return fmt.Sprintf("provided algorithm is not supported %s: ", e.Algorithm) +} + +func NewInvalidAlgorithmError(algorithm string) *InvalidAlgorithmError { + return &InvalidAlgorithmError{Algorithm: algorithm} +} + +// KeysGenerationError definition +type KeysGenerationError struct { + Algorithm string + Err error +} + +func (e *KeysGenerationError) Error() string { + return fmt.Sprintf("error while generating keys pair with algorithm %s: %s", e.Algorithm, e.Err) +} + +func NewKeysGenerationError(algorithm string, err error) *KeysGenerationError { + return &KeysGenerationError{Algorithm: algorithm, Err: err} +} + +// KeysEncodingError definition +type KeysEncodingError struct { + Algorithm string + Err error +} + +func (e *KeysEncodingError) Error() string { + return fmt.Sprintf("error while encoding keys with algorithm %s: %s", e.Algorithm, e.Err) +} + +func NewKeysEncodingError(algorithm string, err error) *KeysEncodingError { + return &KeysEncodingError{Algorithm: algorithm, Err: err} +} + +// DeviceNotFoundError definition +type DeviceNotFoundError struct { + DeviceId string +} + +func (e *DeviceNotFoundError) Error() string { + return fmt.Sprintf("device with id %s not found", e.DeviceId) +} + +func NewDeviceNotFoundError(deviceId string) *DeviceNotFoundError { + return &DeviceNotFoundError{DeviceId: deviceId} +} diff --git a/signing-service-challenge-go/service/transaction_service.go b/signing-service-challenge-go/service/transaction_service.go new file mode 100644 index 0000000..8fb325c --- /dev/null +++ b/signing-service-challenge-go/service/transaction_service.go @@ -0,0 +1,39 @@ +package service + +import ( + "fmt" + "signing-service-challenge/persistence" +) + +type TransactionService interface { + SignTransaction(deviceId string, data string) ([]byte, []byte, error) +} + +type DefaultTransactionService struct { + deviceRepository persistence.DeviceRepository +} + +func NewDefaultTransactionService(deviceRepository persistence.DeviceRepository) *DefaultTransactionService { + return &DefaultTransactionService{deviceRepository: deviceRepository} +} + +func (s *DefaultTransactionService) SignTransaction(deviceId string, data string) ([]byte, []byte, error) { + device, found := s.deviceRepository.GetDeviceById(deviceId) //Get signature device to sign the data + if !found { + return nil, nil, NewDeviceNotFoundError(deviceId) + } + + securedDataToBeSigned := device.BuildSecuredDataToBeSigned(data) //Build secured data to be signed string + signedSecuredData, err := device.Signer.Sign([]byte(securedDataToBeSigned)) //Sign secured data + if err != nil { + return nil, nil, fmt.Errorf("error while signing the data: %w", err) + } + + //Update signature device values and update it in the repository if data was signed successfully + device.IncrementSignatureCounter() + device.UpdateLastSignature(signedSecuredData) + + s.deviceRepository.UpdateDevice(device) + + return signedSecuredData, []byte(securedDataToBeSigned), nil +} diff --git a/signing-service-challenge-go/service/transaction_service_test.go b/signing-service-challenge-go/service/transaction_service_test.go new file mode 100644 index 0000000..352a42d --- /dev/null +++ b/signing-service-challenge-go/service/transaction_service_test.go @@ -0,0 +1,124 @@ +package service + +import ( + "errors" + "signing-service-challenge/domain" + "signing-service-challenge/mocks" + "signing-service-challenge/persistence" + "sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSignTransactionSuccess(t *testing.T) { + mockDeviceRepository := mocks.NewMockDeviceRepository() + mockSigner := &mocks.MockSigner{ + SignFunc: func(dataToBeSigned []byte) ([]byte, error) { + return []byte("test-signature"), nil + }, + } + + mockDevice := &domain.Device{ + Id: "test-id", + Algorithm: domain.RSAAlgorithm, + Label: "Test Device", + Signer: mockSigner, + SignatureCounter: 0, + LastSignature: nil, + } + + mockDeviceRepository.DeviceToReturn = mockDevice + mockDeviceRepository.GetDeviceByIdFound = true + transactionService := NewDefaultTransactionService(mockDeviceRepository) + signedData, securedData, err := transactionService.SignTransaction("test-id", "test data") + + assert.NoError(t, err, "expect no error when signed data successfully") + assert.Equal(t, []byte("test-signature"), signedData, "expect signed data to be correct") + assert.Contains(t, string(securedData), "test data", "expect secured data to contain test data") + assert.GreaterOrEqual(t, mockDeviceRepository.UpdateDeviceCallsCount, 1, "expect device to be updated in the repository") + + assert.Equal(t, 1, mockDevice.SignatureCounter, "expect signature device counter augmented") + assert.Equal(t, []byte("test-signature"), mockDevice.LastSignature, "expect last signature to be correct") +} + +func TestSignTransactionWithDeviceNotFound(t *testing.T) { + mockDeviceRepository := mocks.NewMockDeviceRepository() + mockDeviceRepository.GetDeviceByIdFound = false + + transactionService := NewDefaultTransactionService(mockDeviceRepository) + + signedData, securedData, err := transactionService.SignTransaction("invalid-id", "test-data") + + assert.Error(t, err, "expect error when device not found") + assert.Nil(t, signedData, "expect signed data to be nil") + assert.Nil(t, securedData, "expect secured signed data to be nil") + + var deviceNotFoundError *DeviceNotFoundError + isDeviceNotFoundError := errors.As(err, &deviceNotFoundError) + assert.True(t, isDeviceNotFoundError, "expect error to be of type DeviceNotFoundError") + +} + +func TestSignTransactionWithSignError(t *testing.T) { + mockDeviceRepository := mocks.NewMockDeviceRepository() + mockSigner := &mocks.MockSigner{ + SignFunc: func(dataToBeSigned []byte) ([]byte, error) { + return nil, errors.New("some signing error") + }, + } + + mockDevice := &domain.Device{ + Id: "test-id", + Algorithm: domain.RSAAlgorithm, + Label: "Test device", + Signer: mockSigner, + SignatureCounter: 0, + LastSignature: nil, + } + + mockDeviceRepository.DeviceToReturn = mockDevice + mockDeviceRepository.GetDeviceByIdFound = true + + transactionService := NewDefaultTransactionService(mockDeviceRepository) + + signedData, securedData, err := transactionService.SignTransaction("test-id", "test-data") + + assert.Error(t, err, "expect error when signing fails") + assert.Nil(t, signedData, "expect signed data to be nil") + assert.Nil(t, securedData, "expect secured data to be nil") + + assert.EqualError(t, err, "error while signing the data: some signing error", "expect correct error message") + assert.Equal(t, 0, mockDeviceRepository.UpdateDeviceCallsCount, "expect device not be updated in memory when signing fails") +} + +func TestDefaultTransactionService_ConcurrentSignTransaction(t *testing.T) { + deviceRepository := persistence.NewInmemoryDeviceRepository() + service := NewDefaultTransactionService(deviceRepository) + + device := &domain.Device{ + Id: "test-id", + Label: "test-device", + Algorithm: domain.RSAAlgorithm, + SignatureCounter: 0, + Signer: &mocks.MockSigner{}, + } + deviceRepository.UpdateDevice(device) + + const concurrentTransactions = 50 + var wg sync.WaitGroup + wg.Add(concurrentTransactions) + + for i := 0; i < concurrentTransactions; i++ { + go func() { + defer wg.Done() + _, _, err := service.SignTransaction("test-id", "test-data") + assert.NoError(t, err) + }() + } + + wg.Wait() + + updatedDevice, _ := deviceRepository.GetDeviceById("test-id") + assert.Equal(t, concurrentTransactions, updatedDevice.SignatureCounter, "expect all transactions to be processed") +}