Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support MQTT certificate and the key for the gateway backend #201

Merged
merged 8 commits into from Jan 29, 2018
12 changes: 11 additions & 1 deletion cmd/lora-app-server/main.go
Expand Up @@ -174,7 +174,7 @@ func setRedisPool(c *cli.Context) error {
}

func setHandler(c *cli.Context) error {
h, err := mqtthandler.NewHandler(c.String("mqtt-server"), c.String("mqtt-username"), c.String("mqtt-password"), c.String("mqtt-ca-cert"))
h, err := mqtthandler.NewHandler(c.String("mqtt-server"), c.String("mqtt-username"), c.String("mqtt-password"), c.String("mqtt-ca-cert"), c.String("mqtt-cert"), c.String("mqtt-cert-key"))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c.String("mqtt-cert"), c.String("mqtt-cert-key")

That should be mqtt-tls-cert an mqtt-tls-key I think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh oh, I missed... Sorry.

if err != nil {
return errors.Wrap(err, "setup mqtt handler error")
}
Expand Down Expand Up @@ -600,6 +600,16 @@ func main() {
Usage: "mqtt CA certificate file used by the gateway backend (optional)",
EnvVar: "MQTT_CA_CERT",
},
cli.StringFlag{
Name: "mqtt-tls-cert",
Usage: "mqtt certificate file used by the gateway backend (optional)",
EnvVar: "MQTT_CERT",
},
cli.StringFlag{
Name: "mqtt-tls-key",
Usage: "mqtt key file of certificate used by the gateway backend (optional)",
EnvVar: "MQTT_CERT_KEY",
},
cli.StringFlag{
Name: "as-public-server",
Usage: "ip:port of the application-server api (used by LoRa Server to connect back to LoRa App Server)",
Expand Down
2 changes: 2 additions & 0 deletions docs/content/install/config.md
Expand Up @@ -20,6 +20,8 @@ GLOBAL OPTIONS:
--mqtt-username value mqtt server username (optional) [$MQTT_USERNAME]
--mqtt-password value mqtt server password (optional) [$MQTT_PASSWORD]
--mqtt-ca-cert value mqtt CA certificate file used by the gateway backend (optional) [$MQTT_CA_CERT]
--mqtt-tls-cert value mqtt certificate file used by the gateway backend (optional) [$MQTT_CERT]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$MQTT_CERT --> $MQTT_TLS_CERT, same for $MQTT_CERT_KEY.

--mqtt-tls-key value mqtt key file of certificate used by the gateway backend (optional) [$MQTT_CERT_KEY]
--as-public-server value ip:port of the application-server api (used by LoRa Server to connect back to LoRa App Server) (default: "localhost:8001") [$AS_PUBLIC_SERVER]
--as-public-id value random uuid defining the id of the application-server installation (used by LoRa Server as routing-profile id) (default: "6d5db27e-4ce2-4b2b-b5d7-91f069397978") [$AS_PUBLIC_ID]
--bind value ip:port to bind the api server (default: "0.0.0.0:8001") [$BIND]
Expand Down
79 changes: 59 additions & 20 deletions internal/handler/mqtthandler/mqtt_handler.go
Expand Up @@ -15,6 +15,8 @@ import (

log "github.com/sirupsen/logrus"

"errors"

"github.com/brocaar/lora-app-server/internal/common"
"github.com/brocaar/lora-app-server/internal/handler"
mqtt "github.com/eclipse/paho.mqtt.golang"
Expand All @@ -36,7 +38,7 @@ type MQTTHandler struct {
}

// NewHandler creates a new MQTTHandler.
func NewHandler(server, username, password, cafile string) (handler.Handler, error) {
func NewHandler(server, username, password, cafile, certFile, certKeyFile string) (handler.Handler, error) {
h := MQTTHandler{
dataDownChan: make(chan handler.DataDownPayload),
}
Expand All @@ -48,13 +50,16 @@ func NewHandler(server, username, password, cafile string) (handler.Handler, err
opts.SetOnConnectHandler(h.onConnected)
opts.SetConnectionLostHandler(h.onConnectionLost)

if cafile != "" {
tlsconfig, err := newTLSConfig(cafile)
if err != nil {
log.Fatalf("Error with the mqtt CA certificate: %s", err)
} else {
opts.SetTLSConfig(tlsconfig)
}
tlsconfig, err := newTLSConfig(cafile, certFile, certKeyFile)
if err != nil {
log.WithError(err).WithFields(log.Fields{
"ca_cert": cafile,
"tls_cert": certFile,
"tls_key": certKeyFile,
}).Fatalf("error loading mqtt certificate files")
}
if tlsconfig != nil {
opts.SetTLSConfig(tlsconfig)
}

log.WithField("server", server).Info("handler/mqtt: connecting to mqtt broker")
Expand All @@ -70,23 +75,57 @@ func NewHandler(server, username, password, cafile string) (handler.Handler, err
return &h, nil
}

func newTLSConfig(cafile string) (*tls.Config, error) {
func newTLSConfig(cafile, certFile, certKeyFile string) (*tls.Config, error) {
// Here are three valid options:
Copy link
Owner

@brocaar brocaar Jan 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for commenting on this again, but maybe it is better to remove the whole check at all. E.g. when the user does only provide a tls cert or only a tls key, tls.LoadX509KeyPair(certFile, certKeyFile) will also handle this as an error.

// - Only CA
// - TLS cert + key
// - CA, TLS cert + key
// Ref: https://github.com/brocaar/lora-app-server/pull/201#discussion_r162909271

if (certFile == "" || certKeyFile == "") && (certFile != certKeyFile) { // check exclusive empty string (like XOR)
log.WithFields(log.Fields{
"cafile": cafile,
"certFile": certFile,
"certKeyFile": certKeyFile,
}).Error(`handler/mqtt: TLS config is invalid. Both certFile and certKeyFile must be specified. MQTT TLS configuration allows three patterns:
1. Only CA
2. TLS cert + key
3. CA, TLS cert + key
`)
return nil, errors.New("handler/mqtt: TLS config is invalid. Both certFile and certKeyFile must be specified")
}

if cafile == "" && certFile == "" && certKeyFile == "" {
log.Info("handler/mqtt: TLS config is empty")
return nil, nil
}

tlsConfig := &tls.Config{}

// Import trusted certificates from CAfile.pem.
if cafile != "" {
cacert, err := ioutil.ReadFile(cafile)
if err != nil {
log.Errorf("handler/mqtt: couldn't load cafile: %s", err)
return nil, err
}
certpool := x509.NewCertPool()
certpool.AppendCertsFromPEM(cacert)

cert, err := ioutil.ReadFile(cafile)
if err != nil {
log.Errorf("backend: couldn't load cafile: %s", err)
return nil, err
tlsConfig.RootCAs = certpool // RootCAs = certs used to verify server cert.
}

certpool := x509.NewCertPool()
certpool.AppendCertsFromPEM(cert)
// Import certificate and the key
if certFile != "" && certKeyFile != "" {
kp, err := tls.LoadX509KeyPair(certFile, certKeyFile)
if err != nil {
log.Errorf("handler/mqtt: couldn't load MQTT TLS key pair: %s", err)
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{kp}
}

// Create tls.Config with desired tls properties
return &tls.Config{
// RootCAs = certs used to verify server cert.
RootCAs: certpool,
}, nil
return tlsConfig, nil
}

// Close stops the handler.
Expand Down
70 changes: 68 additions & 2 deletions internal/handler/mqtthandler/mqtt_handler_test.go
@@ -1,8 +1,9 @@
package mqtthandler

import (
"encoding/json"
"testing"

"encoding/json"
"time"

"github.com/brocaar/lora-app-server/internal/common"
Expand All @@ -28,7 +29,7 @@ func TestMQTTHandler(t *testing.T) {
test.MustFlushRedis(common.RedisPool)

Convey("Given a new MQTTHandler", func() {
h, err := NewHandler(conf.MQTTServer, conf.MQTTUsername, conf.MQTTPassword, "")
h, err := NewHandler(conf.MQTTServer, conf.MQTTUsername, conf.MQTTPassword, "", "", "")
So(err, ShouldBeNil)
defer h.Close()
time.Sleep(time.Millisecond * 100) // give the backend some time to connect
Expand Down Expand Up @@ -193,4 +194,69 @@ func TestMQTTHandler(t *testing.T) {
})
})
})

// XXX:
// This test is interested in private method.
// This is not desirable, but this for the sake of simplicity of testing and implementation.
Convey("Given TLS configuration", t, func() {
testCAPemFile := "test_ca.pem"
testCertPemFile := "test_cert.pem"
testCertKeyPemFile := "test_key.pem"

Convey("When parameters that expects nil result, result should be nil", func() {
var params = []struct {
cafile string
certFile string
certKeyFile string
}{
{"", "", ""},
}

for _, p := range params {
tlsConfig, err := newTLSConfig(p.cafile, p.certFile, p.certKeyFile)
So(err, ShouldBeNil)
So(tlsConfig, ShouldBeNil)
}
})

Convey("When invalid parameters given, should raise error and result should be nil", func() {
var params = []struct {
cafile string
certFile string
certKeyFile string
}{
{"", testCertPemFile, ""},
{"", "", testCertKeyPemFile},
{testCAPemFile, testCertPemFile, ""},
{testCAPemFile, "", testCertKeyPemFile},
}

for _, p := range params {
tlsConfig, err := newTLSConfig(p.cafile, p.certFile, p.certKeyFile)
So(err, ShouldNotBeNil)
So(tlsConfig, ShouldBeNil)
}
})

Convey("When parameters that expects meaningful result", func() {
var params = []struct {
cafile string
certFile string
certKeyFile string
rootCAsAssertion func(actual interface{}, expected ...interface{}) string
certificatesAssertion func(actual interface{}, expected ...interface{}) string
}{
{testCAPemFile, "", "", ShouldNotBeNil, ShouldBeNil},
{testCAPemFile, testCertPemFile, testCertKeyPemFile, ShouldNotBeNil, ShouldNotBeNil},
{"", testCertPemFile, testCertKeyPemFile, ShouldBeNil, ShouldNotBeNil},
}

for _, p := range params {
tlsConfig, err := newTLSConfig(p.cafile, p.certFile, p.certKeyFile)
So(err, ShouldBeNil)
So(tlsConfig.RootCAs, p.rootCAsAssertion)
So(tlsConfig.Certificates, p.certificatesAssertion)
}
})
})
}
16 changes: 16 additions & 0 deletions internal/handler/mqtthandler/test_ca.pem
@@ -0,0 +1,16 @@
-----BEGIN CERTIFICATE REQUEST-----
MIICijCCAXICAQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAN7nJ2aEeOJRMjT5Gevrlu37thYfn9nz5M/YXD3x
CYoeUKToRTPF8L8QlsQxTzIV9DOYr6BCUQPtymwJxae+rEc8bbtxcSq+3fI/dBAR
u3CJei1SGR8Zv/X5jM8pWYdNoeNUGO5sxzWJLODNycANn9Bkb9pVtGVi0tysPIdF
TLV8HqoA3vvPRENWpYq9YCvFVWCdOB8ZgwhceBhdmsvyQ3UNzUAKXTOwMLMgfo85
n3vZ7XFrmaUKUrfi02kEkrCkIf18lKY6nleKczZFasDMukqvOUR9PCGKLDM8Zizi
B7Z53MU+L88FGTimrwBt/zIj78ByGBdIMcCInHE96SxgFzkCAwEAAaAAMA0GCSqG
SIb3DQEBCwUAA4IBAQB8S5AjJ6gBqgnyl21exumJ6yq01GRtFPSBETS5ZtePg2XS
Un00AWBcQvR8wo9Ul4Oyg/BlUzhSLV8vy3dRN/ssiM+UyLCDftvcthsdgve3baQ9
EX5gw5EYwE6rJs5WERkuoEamLgqaFzZOohMkPPbtqk5aLXOHRNmIUXRmz+Muqkgg
a7vUvlrnZqX2Tk6RTIaQyo8y4u1/nJPXqTd7bfipam2j3+N34+ssFr0Q/GgaEEuS
Cs5OxniMbSKRPzcyEGf8wu8mkTSaxVCOnPjywdV7ld/UMhc+nCmzd34ffzahR5DS
bwiH9uAGL8hoSeaT+iZA9usLHuuutuPM/hVb28jo
-----END CERTIFICATE REQUEST-----
19 changes: 19 additions & 0 deletions internal/handler/mqtthandler/test_cert.pem
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDBjCCAe4CCQCHfd3pnzIMRzANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB
VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
cyBQdHkgTHRkMB4XDTE4MDEyMzA3NDIxOFoXDTE4MDIyMjA3NDIxOFowRTELMAkG
A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0
IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AN7nJ2aEeOJRMjT5Gevrlu37thYfn9nz5M/YXD3xCYoeUKToRTPF8L8QlsQxTzIV
9DOYr6BCUQPtymwJxae+rEc8bbtxcSq+3fI/dBARu3CJei1SGR8Zv/X5jM8pWYdN
oeNUGO5sxzWJLODNycANn9Bkb9pVtGVi0tysPIdFTLV8HqoA3vvPRENWpYq9YCvF
VWCdOB8ZgwhceBhdmsvyQ3UNzUAKXTOwMLMgfo85n3vZ7XFrmaUKUrfi02kEkrCk
If18lKY6nleKczZFasDMukqvOUR9PCGKLDM8ZiziB7Z53MU+L88FGTimrwBt/zIj
78ByGBdIMcCInHE96SxgFzkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAiHFpOL81
IjcEhifBZnJJvGjskU8Tb9+cMncUhZh7jLYRI6c1OHcSdF3KF7YdDoCTqV5BCHkV
+ogqGRVwZ/2CYToK8tR1oXVIVpd72sKvvtncSkplLVpqThwmq5JOB+wPGXe5eQKa
4uljcoRB5Nf+0qhomuqoO4izqWQNSQFtW7cEnXoEgM2rZolYLTHQMPN1UKMKcTkD
5/1RA4hHoDjFBd9dCUWzZJE4uyzF5HcAS5K4FOCXwvdrA93VSzPxP0ksnQo0rHkG
RiZfEYaLd6rCcuHJJWG45XIRXQK24HvB5R79XnRVt0Uvzq9sW2+t38gKc3KvmIbH
fTSaOkfzXm+l8w==
-----END CERTIFICATE-----
27 changes: 27 additions & 0 deletions internal/handler/mqtthandler/test_key.pem
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpgIBAAKCAQEA3ucnZoR44lEyNPkZ6+uW7fu2Fh+f2fPkz9hcPfEJih5QpOhF
M8XwvxCWxDFPMhX0M5ivoEJRA+3KbAnFp76sRzxtu3FxKr7d8j90EBG7cIl6LVIZ
Hxm/9fmMzylZh02h41QY7mzHNYks4M3JwA2f0GRv2lW0ZWLS3Kw8h0VMtXweqgDe
+89EQ1alir1gK8VVYJ04HxmDCFx4GF2ay/JDdQ3NQApdM7AwsyB+jzmfe9ntcWuZ
pQpSt+LTaQSSsKQh/XyUpjqeV4pzNkVqwMy6Sq85RH08IYosMzxmLOIHtnncxT4v
zwUZOKavAG3/MiPvwHIYF0gxwIiccT3pLGAXOQIDAQABAoIBAQDEt5/QG+1LXnk+
wvCbgskqslBaagJ7KYGv5LRTfhv7JxHo14vrSy9Sj+Netl28SB/CQWgNuTkijINu
oZksl1wXaj81g8JqBRR/LHzTibKweMO4p5HAHsuI23nnggifHmZW5+sw0BNnLe7L
XxJESkHWei00tRqFt5d8ZQzuHLy8FG4zaQpc1O957W91uIj4yjC6Z+NApSRy7Qtw
co/2cIo2a53pxbsyWHs12Wm/OE21cKMZvWtxNprVDlISY41JL6/Qgi8j89DijFst
TueKx+FB80BtlAM7+dRyvt7aGbyhRhC8+irBA8vMqxiz0XjDUV/oc6xzpXTemhyu
878CarnZAoGBAPnTp3iG+rIQ0zMnGyTtBLSvQg+RQVNWkru8J1plLUAVt509oiaN
/BpiekD5CH6NGar0UJcq9oHFIFrV90zsi7e7OAFTmtmRzG3nnvRp1zpeic5LBbVb
KTmELpZrfaUk4nUHQTGcq+SKK8RzZoeJb5juWMQJFrWCKfP/7G5p9LXnAoGBAORp
L5U5RCl2OIPFH86oJJcTmKgs7XilZaQdsCMhmq/4dLMxy4p8t/vxSh3JUFQ00wq8
M9XOKA1pO+Ad5KaUmTGu0J7f2YIxTRxXqVprvyxCj2eo+IuZd1nvQ0W+XK4ndT5+
npEImTjbmK4zXQN6TxJ2wzxRCZp3FQlV2P/u0uXfAoGBAMt5EEBBJ3PZ8joKUrhb
dua2i0ZkluEKdM4Eq8Sa/SThyz99EFD4eWj/5fR/H+T6hPpQrEbCzizZYcW52QZE
7nLBQBcMgeVMM0UcTcFhZtN6ZiCnx8lyqvvWZZ9LgvT7Opn4Q6flo7aqtoT1PH+N
d2AGWDOp913z2rmJKoavM4jnAoGBAM8ga5vgcGVA5YLosS1P4M53YMmw5C+xnPg0
S9Ov13yXzAvrre4JpzX62wEj24pg1Lg5brAF4OA4e6mCsiQ1QK6DHn/T8oRTfN+k
xthOOPBD85NG8Qx2wHp3tAN82sK62WEwpU5UA85BpLTjswdCVI4j0GvT+Odv8U2j
4cJEqk71AoGBAKqvwZ/QXcxsLqF5QBb2wBrleiezci3eLM0h2t7BYZWgAQ7jP3rx
Lv0LVfBFwDF63QA5IekwaWjBBL2LVJF8i4tLC0r6yYdXAfeWtiZnmDNlsJMY3vML
vqemBbFZNqvxwI0DEHMBTqrnjJaf76elPz388jreHfnJMzymEjQcjm6Y
-----END RSA PRIVATE KEY-----
2 changes: 1 addition & 1 deletion internal/handler/multihandler/multi_test.go
Expand Up @@ -66,7 +66,7 @@ func TestHandler(t *testing.T) {
token.Wait()
So(token.Error(), ShouldBeNil)

mqttHandler, err := mqtthandler.NewHandler(conf.MQTTServer, conf.MQTTUsername, conf.MQTTPassword, "")
mqttHandler, err := mqtthandler.NewHandler(conf.MQTTServer, conf.MQTTUsername, conf.MQTTPassword, "", "", "")
So(err, ShouldBeNil)

Convey("Given an organization, application with http integration and node", func() {
Expand Down