Skip to content

Commit

Permalink
Support MQTT client certificate authentication (#201)
Browse files Browse the repository at this point in the history
  • Loading branch information
moznion authored and brocaar committed Jan 29, 2018
1 parent c6ccfad commit cce7403
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 24 deletions.
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-tls-cert"), c.String("mqtt-tls-key"))
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_TLS_CERT]
--mqtt-tls-key value mqtt key file of certificate used by the gateway backend (optional) [$MQTT_TLS_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
63 changes: 43 additions & 20 deletions internal/handler/mqtthandler/mqtt_handler.go
Expand Up @@ -36,7 +36,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 +48,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 +73,43 @@ 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:
// - Only CA
// - TLS cert + key
// - CA, TLS cert + key

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) // here raises error when the pair of cert and key are invalid (e.g. either one is empty)
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
5 changes: 3 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
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

0 comments on commit cce7403

Please sign in to comment.