mosquitto-go-auth編譯與使用

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計算。至於日後會不會改變?(也許不會吧)這是要特別注意的地方。


參考資源

  1. Eclipsea Mosquitto – https://github.com/eclipse/mosquitto
  2. mosquitto-auth-plugin – https://github.com/jpmens/mosquitto-auth-plug
  3. mosquitto-go-auth – https://github.com/iegomez/mosquitto-go-auth

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *