mosquitto-go-auth編譯與使用
mosquitto是一個受歡迎的輕量MQTT broker,雖然本身具備了pwfile、aclfile等使用者驗證機制,但使用者數增加、或是使用情境較複雜一點的時候,用起來就沒有那麼彈性了,而且修改完pwfile、aclfile還需要重啟服務,如果服務不適合中斷,那可就尷尬了。
為了做更有彈性的使用者驗證管理,有些人會考慮自行修改mosquitto原始碼(畢竟mosquitto是開源軟體),或者是另外撰寫mosquitto plug-in來處理驗證這一塊(是的!mosquitto有plug-in interface),例如jpmens的mosquitto-auth-plugin,就是一個廣受觀迎的mosquitto驗證plug-in,在Github上的fork數甚至達到487!不過這有一部份原因應該是原作者在2018年底就不再繼續維護此專案,大家有issue或是新需求就fork出來各自努力。😥
不過別難過得太早,早在2017年開始,iegomez便開始了一個新的mosquitto驗證plug-in專案,稱為mosquitto-go-auth,也就是本文要介紹的軟體。
簡介
根據mosquitto-go-auth專案的README.md所述,這個專案主要是受到jpmens的mosquitto-auth-plug專案啟發,採用Go語言開發,並利用cgo曝露出符合mosquitto plug-in規範的連結介面,供mosquitto與此plug-in動態連結。
mosquitto-go-auth主要具備以下幾個優點(個人看法):
- 使用Go開發,擴充、改寫、編譯皆比原本以C撰寫來得方便
- 原mosquitto-auth-plug支援的backend皆有支援(如:Redis、MySQL、JWT、HTTP、……等),甚至更完善
- 與最新版的Mosquitto 2.0相容
- 持續維護中!
編譯
要編譯mosquitto-go-auth專案,需準備以下項目:
- Go工具包(建議直接用最新版,本文撰寫之時為1.19)
- mosquitto原始碼
- mosquitto-go-auth原始碼
工作目錄如下:
- Go: /home/kaffa9/ws/go1.19
- mosquitto: /home/kaffa9/ws/mosquitto
- mosquitto-go-auth: /home/kaffa9/ws/mosquitto-go-auth
- 測試目錄: /home/kaffa9/ws/mosquitto-test
在mosquitto-go-auth工作目錄下,先以編輯器開啟Makefile:
CFLAGS := -I/usr/local/include -fPIC LDFLAGS := -shared UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) LDFLAGS += -undefined dynamic_lookup endif all: @echo "Bulding for $(UNAME_S)" env CGO_CFLAGS="$(CFLAGS)" go build -buildmode=c-archive go-auth.go env CGO_LDFLAGS="$(LDFLAGS)" go build -buildmode=c-shared -o go-auth.so go build pw-gen/pw.go test: cd plugin && make go test ./backends ./cache ./hashing -v -count=1 rm plugin/*.so test-backends: cd plugin && make go test ./backends -v -failfast -count=1 rm plugin/*.so test-cache: go test ./cache -v -failfast -count=1 test-hashing: go test ./hashing -v -failfast -count=1 service: @echo "Generating gRPC code from .proto files" @go generate grpc/grpc.go clean: rm -f go-auth.h rm -f go-auth.so rm -f pw
進行以下修改:
- 在第1行加上mosquitto標頭檔的路徑:-I../mosquitto/src -I../mosquitto/lib
- 在第12行加上CGO_LDFLAGS=”$(LDFLAGS)”
- 在第13行加上CGO_CFLAGS=”$(CFLAGS)”
CFLAGS := -I/usr/local/include -I../mosquitto/src -I../mosquitto/lib -fPIC LDFLAGS := -shared UNAME_S := $(shell uname -s) ifeq ($(UNAME_S),Darwin) LDFLAGS += -undefined dynamic_lookup endif all: @echo "Bulding for $(UNAME_S)" env CGO_CFLAGS="$(CFLAGS)" CGO_LDFLAGS="$(LDFLAGS)" go build -buildmode=c-archive go-auth.go env CGO_CFLAGS="$(CFLAGS)" CGO_LDFLAGS="$(LDFLAGS)" go build -buildmode=c-shared -o go-auth.so go build pw-gen/pw.go test: cd plugin && make go test ./backends ./cache ./hashing -v -count=1 rm plugin/*.so test-backends: cd plugin && make go test ./backends -v -failfast -count=1 rm plugin/*.so test-cache: go test ./cache -v -failfast -count=1 test-hashing: go test ./hashing -v -failfast -count=1 service: @echo "Generating gRPC code from .proto files" @go generate grpc/grpc.go clean: rm -f go-auth.h rm -f go-auth.so rm -f pw
如此一下,Go編譯時才找得到相依的mosquitto標頭檔。
設定一下Go工具路徑:
export PATH=/home/kaffa9/ws/go1.19/bin:$PATH
就可以開始編譯囉!
make
編譯完成後,可得以下檔案:
- go-auth.so : plug-in
- pw : 密碼工具
設定與運行(以Redis為例)
首先將mosquitto執行檔以及go-auth.so複製到測試目錄(/home/kaffa9/ws/mosquitto-test)中。
要讓plug-in順利運行,必須在mosquitto.conf中進行設定,以下為參考設定值:
listener 1883 auth_plugin /home/kaffa9/ws/mosquitto-test/go-auth.so auth_opt_backends redis auth_opt_hasher pbkdf2 # password hashing type auth_opt_hasher_salt_size 16 # salt bytes length auth_opt_hasher_iterations 100000 # number of iterations auth_opt_hasher_keylen 64 # key length auth_opt_hasher_algorithm sha512 # hashing algorithm, either sha512 (default) or sha256 auth_opt_hasher_salt_encoding base64 # salt encoding, either base64 (default) or utf-8 auth_opt_redis_host localhost auth_opt_redis_port 6379 auth_opt_redis_db 1 auth_opt_redis_password myPa55w0rd auth_opt_redis_disable_superuser true
我們將mosquitto運行在port 1883,指定驗證plug-in路徑,並額外進行以下設定:
- auth_opt_backends : 指定使用redis
- auth_opt_hasher : 指定密碼採用pbkdf2格式
- auth_opt_hasher_salt_size : 指定salt長度為16
- auth_opt_hasher_iterations : 指定PBKDF2迭代次數為100000
- auth_opt_hasher_keylen : 指定金鑰長度為64
- auth_opt_hasher_algorithm : 指定HMAC雜湊演算法為sha512
- auth_opt_hasher_salt_encoding : 指定salt儲存格式為base64
- auth_opt_redis_host : Redis連線位址
- auth_opt_redis_port : Redis連線port
- auth_opt_redis_db : Redis資料庫編號
- auth_opt_redis_password : Redis密碼
- auth_opt_redis_disable_superuser : 是否停用superuser功能
接下來可以使用pw工具為使用者,依照mosquitto.conf裡設定的條件(salt_size、iterations、keylen、……)產生密碼,這裡我們產生foo123和bar123,共2組密碼,待會兒給foo和bar這2個帳號使用:
$ ./pw -a sha512 -e base64 -h pbkdf2 -i 100000 -l 64 -s 16 -p foo123 PBKDF2$sha512$100000$9Ey+5BSIyYJCTnNFJm7NPA==$S7BZ8loLtIergIybLQbOf45iJsubksB10VHAnBkZ93/FyfS9nXuYH34Rv2mIhC8sVg24jzjs+fwg9IayGNSr7g== $ ./pw -a sha512 -e base64 -h pbkdf2 -i 100000 -l 64 -s 16 -p bar123 PBKDF2$sha512$100000$Yd+W79o4NRrvl+c5uiYceA==$Op/YJbn0xs7nd9tFZ9Eumstj/HBn4A+nJydYEsScucko1rdDXJOIy83lV2CbiZNXfTWPFgtG2gv1ju0M/TI0TQ==
然後將帳號密碼的對應關係寫入Redis中(以下在redis-cli中操作):
選擇使用資料庫1 127.0.0.1:6379> SELECT 1 新增foo 127.0.0.1:6379[1]> SET foo PBKDF2$sha512$100000$9Ey+5BSIyYJCTnNFJm7NPA==$S7BZ8loLtIergIybLQbOf45iJsubksB10VHAnBkZ93/FyfS9nXuYH34Rv2mIhC8sVg24jzjs+fwg9IayGNSr7g== OK 新增bar 127.0.01:6379[1]> SET bar PBKDF2$sha512$100000$Yd+W79o4NRrvl+c5uiYceA==$Op/YJbn0xs7nd9tFZ9Eumstj/HBn4A+nJydYEsScucko1rdDXJOIy83lV2CbiZNXfTWPFgtG2gv1ju0M/TI0TQ== OK
接著設定ACL:
設定ACL (subscribe) 127.0.0.1:6379[1]> SADD foo:sacls "mailbox/foo/#" OK 設定ACL (read) 127.0.0.1:6379[1]> SADD foo:racls "mailbox/foo/#" OK 設定ACL (write) 127.0.0.1:6379[1]> SADD foo:wacls "mailbox/foo/#" OK 設定ACL (readwrite) 127.0.0.1:6379[1]> SADD foo:rwacls "mailbox/foo/#" OK //bar的部份如法泡製
接著讓mosquitto正式上場:
./mosquitto -c ./mosquitto.conf 1660441737: mosquitto version 2.0.14 starting 1660441737: Config loaded from ./mosquitto.conf. 1660441737: Loading plugin: /home/kaffa9/ws/mosquitto-test/go-auth.so 1660441737: ├── Username/password checking enabled. 1660441737: ├── TLS-PSK checking enabled. 1660441737: └── Extended authentication not enabled. INFO[2022-08-14T01:48:57Z] Backend registered: Redis INFO[2022-08-14T01:48:57Z] registered acl checker: redis INFO[2022-08-14T01:48:57Z] registered user checker: redis INFO[2022-08-14T01:48:57Z] registered superuser checker: redis INFO[2022-08-14T01:48:57Z] No cache set. 1660441737: Opening ipv4 listen socket on port 1883. 1660441737: Opening ipv6 listen socket on port 1883. 1660441737: mosquitto version 2.0.14 running
之後就可以使用MQTT client來測試看看啦!
給「原mosquitto-auth-plug使用者」注意事項
原mosquitto-auth-plug密碼格式只支援PBKDF2,但使用時不需特別指定salt和key長度,mosquitto-auth-plug驗證時會檢查interation數以及salt、key長度,自動往回推算(如下列C程式碼,第11行tokenize取出PDFBK2密碼hash的各部位,第19行計算key長度、第39行計算salt長度)。
int pbkdf2_check(char *password, char *hash) { char *sha, *salt, *h_pw; int iterations, saltlen, blen; char *b64, *keybuf; unsigned char *out; int match = FALSE; const EVP_MD *evpmd; int keylen, rc; if (detoken(hash, &sha, &iterations, &salt, &h_pw) != 0) return match; /* Determine key length by decoding base64 */ if ((keybuf = malloc(strlen(h_pw) + 1)) == NULL) { fprintf(stderr, "Out of memory\n"); return FALSE; } keylen = base64_decode(h_pw, keybuf); if (keylen < 1) { free(keybuf); return (FALSE); } free(keybuf); if ((out = malloc(keylen)) == NULL) { fprintf(stderr, "Cannot allocate out; out of memory\n"); return (FALSE); } #ifdef RAW_SALT char *rawSalt; if ((rawSalt = malloc(strlen(salt) + 1)) == NULL) { fprintf(stderr, "Out of memory\n"); return FALSE; } saltlen = base64_decode(salt, rawSalt); if (saltlen < 1) { return (FALSE); } free(salt); salt = rawSalt; rawSalt = NULL; #else saltlen = strlen((char *)salt); #endif #ifdef PWDEBUG fprintf(stderr, "sha =[%s]\n", sha); fprintf(stderr, "iterations =%d\n", iterations); fprintf(stderr, "salt =[%s]\n", salt); fprintf(stderr, "salt len =[%d]\n", saltlen); fprintf(stderr, "h_pw =[%s]\n", h_pw); fprintf(stderr, "kenlen =[%d]\n", keylen); #endif evpmd = EVP_sha256(); if (strcmp(sha, "sha1") == 0) { evpmd = EVP_sha1(); } else if (strcmp(sha, "sha512") == 0) { evpmd = EVP_sha512(); } rc = PKCS5_PBKDF2_HMAC(password, strlen(password), (unsigned char *)salt, saltlen, iterations, evpmd, keylen, out); if (rc != 1) { goto out; } blen = base64_encode(out, keylen, &b64); if (blen > 0) { int i, diff = 0, hlen = strlen(h_pw); #ifdef PWDEBUG fprintf(stderr, "HMAC b64 =[%s]\n", b64); #endif /* "manual" strcmp() to ensure constant time */ for (i = 0; (i < blen) && (i < hlen); i++) { diff |= h_pw[i] ^ b64[i]; } match = diff == 0; if (hlen != blen) match = 0; free(b64); } out: free(sha); free(salt); free(h_pw); free(out); return match; }
然而在mosquitto-go-auth中,會發現雖然要求在mosquitto.conf中指定hmac演算法、iteration、salt len、keylen,但經過trace原始碼發現,目前仍是仿照mosquitto-auth-plug,由取得的hash做tokenize,分別得到HMAC演算法、iteration、salt len、keylen等值後,再將客戶端的明文密碼進行PBKDF2計算。至於日後會不會改變?(也許不會吧)這是要特別注意的地方。
參考資源
- Eclipsea Mosquitto – https://github.com/eclipse/mosquitto
- mosquitto-auth-plugin – https://github.com/jpmens/mosquitto-auth-plug
- mosquitto-go-auth – https://github.com/iegomez/mosquitto-go-auth