Skip to content

Commit

Permalink
[#3] MQTTS support in the software-update's local connection
Browse files Browse the repository at this point in the history
Signed-off-by: Antonia Avramova <antonia.avramova@bosch.io>
  • Loading branch information
antonia-avramova committed Oct 31, 2023
1 parent 75877be commit 24e2541
Show file tree
Hide file tree
Showing 12 changed files with 280 additions and 1 deletion.
22 changes: 22 additions & 0 deletions internal/edge.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ package feature

import (
"encoding/json"
"net/url"

"github.com/eclipse-kanto/software-update/internal/logger"
"github.com/eclipse-kanto/software-update/internal/util/tls"

MQTT "github.com/eclipse/paho.mqtt.golang"
"github.com/google/uuid"
Expand Down Expand Up @@ -58,6 +60,17 @@ func newEdgeConnector(scriptSUPConfig *ScriptBasedSoftwareUpdatableConfig, ecl e
if len(scriptSUPConfig.Username) > 0 {
opts = opts.SetUsername(scriptSUPConfig.Username).SetPassword(scriptSUPConfig.Password)
}
u, err := url.Parse(scriptSUPConfig.Broker)
if err != nil {
return nil, err
}
if isConnectionSecure(u.Scheme) {
tlsConfig, err := tls.NewTLSConfig(scriptSUPConfig.CACert, scriptSUPConfig.Cert, scriptSUPConfig.Key)
if err != nil {
return nil, err
}
opts.SetTLSConfig(tlsConfig)
}

p := &EdgeConnector{mqttClient: MQTT.NewClient(opts), edgeClient: ecl}
if token := p.mqttClient.Connect(); token.Wait() && token.Error() != nil {
Expand Down Expand Up @@ -98,6 +111,15 @@ func newEdgeConnector(scriptSUPConfig *ScriptBasedSoftwareUpdatableConfig, ecl e
return p, nil
}

func isConnectionSecure(schema string) bool {
switch schema {
case "wss", "ssl", "tls", "mqtts", "mqtt+ssl", "tcps":
return true
default:
}
return false
}

// Close the EdgeConnector
func (p *EdgeConnector) Close() {
if p.cfg != nil {
Expand Down
3 changes: 3 additions & 0 deletions internal/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ type ScriptBasedSoftwareUpdatableConfig struct {
Broker string
Username string
Password string
CACert string
Cert string
Key string
StorageLocation string
FeatureID string
ModuleType string
Expand Down
3 changes: 3 additions & 0 deletions internal/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ type cfg struct {
Broker string `json:"broker" def:"tcp://localhost:1883" descr:"Local MQTT broker address"`
Username string `json:"username" descr:"Username for authorized local client"`
Password string `json:"password" descr:"Password for authorized local client"`
CACert string `json:"caCert" descr:"A PEM encoded CA certificates file for MQTT broker connection"`
Cert string `json:"cert" descr:"A PEM encoded certificate file to authenticate to the MQTT server/broker"`
Key string `json:"key" descr:"A PEM encoded unencrypted private key file to authenticate to the MQTT server/broker"`
StorageLocation string `json:"storageLocation" def:"." descr:"Location of the storage"`
FeatureID string `json:"featureId" def:"SoftwareUpdatable" descr:"Feature identifier of SoftwareUpdatable"`
ModuleType string `json:"moduleType" def:"software" descr:"Module type of SoftwareUpdatable"`
Expand Down
9 changes: 9 additions & 0 deletions internal/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,9 @@ func TestFlagsHasHigherPriority(t *testing.T) {
// 3. Test with all flags are applied instead of default values if no configuration JSON is provided
expectedFlagBroker := "host:1234"
expectedArtifact := "TestArchive"
expectedCACert := "TestCaCert.crt"
expectedCert := "TestCert.cert"
expectedKey := "TestKey.key"
expectedFeatureID := "TestFeature"
expectedInstall := "TestInstall"
expectedServerCert := "TestCert"
Expand All @@ -152,6 +155,9 @@ func TestFlagsHasHigherPriority(t *testing.T) {
setFlags([]string{
c(flagBroker, expectedFlagBroker),
c(flagArtifactType, expectedArtifact),
c(flagCACert, expectedCACert),
c(flagCert, expectedCert),
c(flagKey, expectedKey),
c(flagFeatureID, expectedFeatureID),
c(flagInstall, expectedInstall),
c(flagCert, expectedServerCert),
Expand Down Expand Up @@ -180,6 +186,9 @@ func TestFlagsHasHigherPriority(t *testing.T) {
Broker: expectedFlagBroker,
Username: expectedUsername,
Password: expectedPassword,
CACert: expectedCACert,
Cert: expectedCert,
Key: expectedKey,
ServerCert: expectedServerCert,
StorageLocation: expectedStorageLocation,
InstallCommand: command{cmd: expectedInstall},
Expand Down
22 changes: 22 additions & 0 deletions internal/util/tls/testdata/ca.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDkzCCAnugAwIBAgIUbD0mjn2x1H5VKi54xgjfPsEugzAwDQYJKoZIhvcNAQEL
BQAwWTELMAkGA1UEBhMCQkcxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X
DTIyMTAyNzE1MDk0OFoXDTI3MTAyNzE1MDk0OFowWTELMAkGA1UEBhMCQkcxEzAR
BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5
IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAqRZyG9gywFvplpfPq60X7JYtkbj7OF+tOfixF9FkGDJ9Hhh4LoMv
W+rdLnxEHfI4Vicn0Wj3r/Ra1bd9yo8hzGiYdNliH9kegSbjB2Xx8N4yTCqaiZ9E
JQLcstQeXHEP3YwPXLnNfTOmbQPAbC2T9J+USlmolG1qpkuU5rQVC/sjW5M8MOmN
TuKEU6pds/j8GQKhQmsIHddwfypnBDilpYOotgeMwDqsyM4+zdSXbFaNmYoh3Tjb
FSN0WJTdPe7uv+nG03NZe6dHvN4C/8Z5uBIkeLw8yrO/2Wb7aAxwtpK3Wswzlya9
TAwALiOI+hWuIfUmkoQapskIMhcRAnpMxwIDAQABo1MwUTAdBgNVHQ4EFgQUH2fo
QuQyEoszAL3vBOBqs8OLg7YwHwYDVR0jBBgwFoAUH2foQuQyEoszAL3vBOBqs8OL
g7YwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAYNY+8FNt9Pr/
I6NYzY56opMIXmMErRMBN4i5WkuZxXp7gaqPNrx42O7gaWJz7dwwGUmrb0eHyOIm
XetphU6cW1vNgaXPOggacp2wRJ6AGJn1+v/hfWU6sPt0XWPM9p5umoCeYJeZ1UlE
uodsUGEQc7b/ODROObPHAFc/18nChoiPylXtB5TcgdyzalzhL/d8B/c4QZJwvT+W
L+8IoChNQzeH4yCgoDZXaQpRfrnGjLyrpx4dojNBYd/rJsGNZa+wMwzhFZU3f3QY
jeZnnp+nVBw+/L3q/FVwCee/RsYiR797OL7wyPAJPGd4iqbh09Hv0B2YgIih86X6
giPwyRzg7g==
-----END CERTIFICATE-----
27 changes: 27 additions & 0 deletions internal/util/tls/testdata/certificate.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN CERTIFICATE-----
MIIEjzCCAncCFAEWn/QU1vzg07IeNzhaX/6pNlTeMA0GCSqGSIb3DQEBCwUAMIGD
MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2Fu
IEZyYW5jaXNjbzEVMBMGA1UECgwMRXhhbXBsZSBJbmMuMRYwFAYDVQQLDA1JVCBE
ZXBhcnRtZW50MRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20wHhcNMjEwNTEzMDg0
NTU3WhcNMjExMDEwMDg0NTU3WjCBgzELMAkGA1UEBhMCVVMxFjAUBgNVBAgMDU1h
c3NhY2h1c2V0dHMxDzANBgNVBAcMBkJvc3RvbjEVMBMGA1UECgwMRXhhbXBsZSBJ
bmMuMRYwFAYDVQQLDA1IUiBEZXBhcnRtZW50MRwwGgYDVQQDDBN3d3cuZXhhbXBs
ZS1pbmMuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApmF7J4WB
BujYYNt86YHSWrv3ffX3Odt57y4kqhWuMM8VcA2RXTlJO3SXX8xLF/+lsaZZJmfg
xR1tJ0hKfBYt78H03bjrylWLOQoRqlyVuz0SF3ueR9vx3mPIO0F0E3Q2mC4SLHbr
5kW4aj3/NszLzgvZPbOchfcktdCd1vME+pM8lPPY6z8qWJzlWjOYdymWUV8z9qlA
c3VCnYQ+UwJY51vTcMPpalByrMPWkiNic+9Onl8KHCik27vNMIVVLYLa6763UwU8
qCKT+jZj6nlCT8w5oqoJNsaSd/EGGFtGR+qZj6V2TrsI2cNknyY7Qf/QN0+zH0nm
XsOxK6jW8q7RGwIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQBe/kcT2L54PxZqb3GU
liwYJGjB+9fkqTyMwglt8dAm3it9F/POyXtoKB8a1AuaZ/FJlR+AUOFv+f3i0ZnE
Ek0OAsllVPclv7HhywD1HzrbLh0PreGsBnYgyrW7qZKAfevus0U0GrjhcrY7zCoA
EBFWWqcWqhRCFXYwgI13ZNLhYl7r+NIWLza1bPcnWVfY2g19/nctR53ZFFiVkvlk
FCYGat0SPQWvjFIKaCNQQL6IZSxqk95W87kEWrac9A+bQzENpWLfwu86O5r6vKNK
pHmd47Sy8hyhf75/SEOQuWBEkgT+sPXU7TykvFB8kzO1Wmsz7D2/d5pvkjZF31dQ
m4ZHIuclPOETtAwiY13dI94vAhgruK0FRFn7jyfePn20CFqUCOO9cEQytysCO/n7
4xJPbIVcvUO825Kbos71OWfNkLEi1tlLkFpe73/rSXnZRWweqAThrY7jxGxhveI4
iYrOOYEqGdM6VfLvVhYXhsc/MDqiqJLdbhQAS/lE8bJrnVnVRYnnb0ExfNTwqVU3
8YZB6JbT+j9556c675j0sfa0J8qgDRHsWR7EG5u5wENnDH/s142dYcjJ5S6tCumM
K/GExzmrJFHmLfqKTSxAquMYvDVufICcklL067DacRJKHkyx5KvKkgc3R8rTaX2o
D+0ioElFJXVQ7rULiWzxZs+Gdw==
-----END CERTIFICATE-----
Empty file.
1 change: 1 addition & 0 deletions internal/util/tls/testdata/invalid.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
---invalid data---
27 changes: 27 additions & 0 deletions internal/util/tls/testdata/key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEApmF7J4WBBujYYNt86YHSWrv3ffX3Odt57y4kqhWuMM8VcA2R
XTlJO3SXX8xLF/+lsaZZJmfgxR1tJ0hKfBYt78H03bjrylWLOQoRqlyVuz0SF3ue
R9vx3mPIO0F0E3Q2mC4SLHbr5kW4aj3/NszLzgvZPbOchfcktdCd1vME+pM8lPPY
6z8qWJzlWjOYdymWUV8z9qlAc3VCnYQ+UwJY51vTcMPpalByrMPWkiNic+9Onl8K
HCik27vNMIVVLYLa6763UwU8qCKT+jZj6nlCT8w5oqoJNsaSd/EGGFtGR+qZj6V2
TrsI2cNknyY7Qf/QN0+zH0nmXsOxK6jW8q7RGwIDAQABAoIBAH09m6qgQAOnellO
XrSW2HUcUKwsXjDbGOoF3et57mknOIfkbqux14I9vUSLT2t9MIiNI0ZZo0Q9ZlDP
heHqACId6eiMrlDcG7SP88Q9dShATEII95g349T3X13bYzjRndbntx5pViE8EhlH
GblyZ2duW9SqQwREiQmjQ2zt+a1zuKUAGdysS+4101UHUj6tC/RjiNN/TCXXRJIX
GOJ4WFLY+f2bXgSmqbK7wqN9nPxmxl/+bv4hO32Gsv06ejuy/6+GFJZa+n3ASU2r
/ptr4vgCK+t6I0OWVTpvYUboEwAXam4JfAu12zLtczfXVFJjzklUvSClIG9aJ6DP
2B3LEuECgYEA1RenCuVl6sM2o858X8iOLTMuCZWX5VFKY1+vvLwB/+CBfy7/dDYw
lbv+xaots0rY9Wn784ewi9zJbdnXE1YNgj0utIMHzylXvTolnDYsoO7SYpIwqtpa
PyzPcAV3Khkd5LGe1hf9VmOJfTF/563ztLXip0HUeIvzgB/maqfAQW8CgYEAx+H4
GZ3ycdL03x7Zvp5g5yDPZGvxqVIEIFmliagEFyBgqogbXuOvsvZDTg+gKV5/QyVg
FWokz5VtC6U9UcWfF7LJses/Hsedh9IeICd9UIzkey8UmS2mDBeGfTHVTNuczml5
VzmTK8jGUO5aRVRyOt0tqVf6Oozo8ImdI1fOPRUCgYBI4p4wC+agNcUqoiXIXUDE
FQ1aGeCqfvOCqefiFixY6OFiLyERDrfvfy3VTi/zc1ZiGq4izfaE4C/Fcw0tf/F+
6o5fD7JMGUf5YTocBCufoBA1xur+hVD46srI9hWcQJsI7ff2Ip50Pfd46sVk6QrC
dLPhoZKa6MOQv1iAgoAv4QKBgQDAfiO6N9vmNizQOxujcU8NBxHzOekvEOccaHj9
Cqt1wh6V3CHPziHEjVjf8jhh3rlcZsATn3b32oV7c5SMDW9bGTkYeN7+u2pABOAy
QxVx312iLAMASW/hsT45jyZFsDFgrz7F+5J51g72nbSdk+e2PI7eyPUYMd+a1kxY
XxUkyQKBgBde+jg2UjGHAfW6ZWSPHWvi74oD42VzbJg6/o/KFaZGEyHdKWgrwBW7
8wNyNTMOMSHyuvtMnc5oLd492aO2a+yurgzt7yIelVuphSKda0wd/0PljKb0lwlb
VzldR4bXolbyjNXky1KWmvSNkmbPFNGSCeyHmKQgSLXKGuWbj4ic
-----END RSA PRIVATE KEY-----
59 changes: 59 additions & 0 deletions internal/util/tls/tls_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) 2023 Contributors to the Eclipse Foundation
//
// See the NOTICE file(s) distributed with this work for additional
// information regarding copyright ownership.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0

package tls

import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
)

// NewTLSConfig initializes the TLS.
func NewTLSConfig(rootCert, cert, key string) (*tls.Config, error) {
caCert, err := os.ReadFile(rootCert)
if err != nil {
return nil, fmt.Errorf("failed to load CA: %s", err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return nil, fmt.Errorf("failed to parse CA %s", rootCert)
}

tlsConfig := &tls.Config{
InsecureSkipVerify: false,
RootCAs: caCertPool,
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS13,
CipherSuites: supportedCipherSuites(),
}

if len(cert) > 0 || len(key) > 0 {
cert, err := tls.LoadX509KeyPair(cert, key)
if err != nil {
return nil, fmt.Errorf("failed to load X509 key pair: %s", err)
}
tlsConfig.Certificates = []tls.Certificate{cert}
}

return tlsConfig, nil
}

func supportedCipherSuites() []uint16 {
cs := tls.CipherSuites()
cid := make([]uint16, len(cs))
for i := range cs {
cid[i] = cs[i].ID
}
return cid
}
103 changes: 103 additions & 0 deletions internal/util/tls/tls_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright (c) 2023 Contributors to the Eclipse Foundation
//
// See the NOTICE file(s) distributed with this work for additional
// information regarding copyright ownership.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0

package tls

import (
"crypto/tls"
"errors"
"fmt"
"path/filepath"
"testing"
)

const (
nonExisting = "nonexisting.test"
invalidFile = "testdata/invalid.pem"
caCertPath = "testdata/ca.crt"
certPath = "testdata/certificate.pem"
keyPath = "testdata/key.pem"
caError = "failed to load CA: open %s: no such file or directory"
keyPairError = "failed to load X509 key pair: open %s: no such file or directory"
invalidError = "failed to load X509 key pair: tls: failed to find any PEM data in key input"
)

func TestNewTLSConfig(t *testing.T) {
dirAbsPath, _ := filepath.Abs("./")

tests := map[string]struct {
CACert string
Cert string
Key string
ExpectedError error
}{
"valid_config_with_credentials": {CACert: caCertPath, Cert: certPath, Key: keyPath, ExpectedError: nil},
"valid_config_no_credentials": {CACert: caCertPath, Cert: "", Key: "", ExpectedError: nil},
"no_files_provided": {CACert: "", Cert: "", Key: "", ExpectedError: fmt.Errorf(caError, "")},
"non_existing_ca_file": {CACert: nonExisting, Cert: "", Key: "", ExpectedError: fmt.Errorf(caError, nonExisting)},
"invalid_ca_file": {CACert: invalidFile, Cert: certPath, Key: keyPath, ExpectedError: fmt.Errorf("failed to parse CA %s", invalidFile)},
"invalid_ca_file_arg": {CACert: "\\\000", Cert: certPath, Key: keyPath, ExpectedError: errors.New("failed to load CA: open \\\000: invalid argument")},
"not_abs_cert_file_provided": {CACert: caCertPath, Cert: nonExisting, Key: "", ExpectedError: fmt.Errorf(keyPairError, nonExisting)},
"cert_is_directory": {CACert: caCertPath, Cert: dirAbsPath, Key: "", ExpectedError: fmt.Errorf("failed to load X509 key pair: read %s: is a directory", dirAbsPath)},
"no_key_file_provided": {CACert: caCertPath, Cert: certPath, Key: "", ExpectedError: fmt.Errorf(keyPairError, "")},
"not_abs_key_file_provided": {CACert: caCertPath, Cert: certPath, Key: nonExisting, ExpectedError: fmt.Errorf(keyPairError, nonExisting)},
"empty_key_file_provided": {CACert: caCertPath, Cert: certPath, Key: "testdata/empty.crt", ExpectedError: errors.New(invalidError)},
"cert_file_instead_key": {CACert: caCertPath, Cert: certPath, Key: caCertPath, ExpectedError: fmt.Errorf("failed to load X509 key pair: tls: found a certificate rather than a key in the PEM for the private key")},
"invalid_key_file_provided": {CACert: caCertPath, Cert: certPath, Key: invalidFile, ExpectedError: errors.New(invalidError)},
}

for testName, testCase := range tests {
t.Run(testName, func(t *testing.T) {
cfg, err := NewTLSConfig(testCase.CACert, testCase.Cert, testCase.Key)
if testCase.ExpectedError != nil {
if testCase.ExpectedError.Error() != err.Error() {
t.Fatalf("expected error : %s, got: %s", testCase.ExpectedError, err)
}
if cfg != nil {
t.Fatalf("expected nil, got: %v", cfg)
}
} else {
if err != nil {
t.Fatal(err)
}
if len(cfg.Certificates) == 0 && testCase.Cert != "" && testCase.Key != "" {
t.Fatal("certificates length must not be 0")
}
if len(cfg.CipherSuites) == 0 {
t.Fatal("cipher suites length must not be 0")
}
// assert that cipher suites identifiers are contained in tls.CipherSuites
for _, csID := range cfg.CipherSuites {
if !func() bool {
for _, cs := range tls.CipherSuites() {
if cs.ID == csID {
return true
}
}
return false
}() {
t.Fatalf("cipher suite %d is not implemented", csID)
}
}
if cfg.InsecureSkipVerify {
t.Fatal("skip verify is set to true")
}
if cfg.MinVersion != tls.VersionTLS12 {
t.Fatalf("invalid min TLS version %d", cfg.MinVersion)
}
if cfg.MaxVersion != tls.VersionTLS13 {
t.Fatalf("invalid max TLS version %d", cfg.MaxVersion)
}
}
})
}
}
5 changes: 4 additions & 1 deletion internal/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ const (
flagBroker = "broker"
flagUsername = "username"
flagPassword = "password"
flagCACert = "cACert"
flagCert = "cert"
flagKey = "key"
flagStorageLocation = "storageLocation"
flagFeatureID = "featureId"
flagModuleType = "moduleType"
Expand All @@ -51,7 +54,7 @@ const (
flagLogFileSize = "logFileSize"
flagLogFileCount = "logFileCount"
flagLogFileMaxAge = "logFileMaxAge"
flagCert = "serverCert"
flagServerCert = "serverCert"
flagRetryCount = "downloadRetryCount"
flagRetryInterval = "downloadRetryInterval"
)
Expand Down

0 comments on commit 24e2541

Please sign in to comment.