test_auth.sh (9186B)
1 #!/bin/sh 2 # Password hashing: PBKDF2 correctness against OpenSSL, adapted-base64 3 # round-trips, and the role model end to end. 4 set -eu 5 HERE=$(cd "$(dirname "$0")" && pwd) 6 ROOT=$(cd "${HERE}/../.." && pwd) 7 . "${HERE}/../helpers.sh" 8 9 echo "==> test_auth: password hashing and roles" 10 11 BUILD_DIR=$(ensure_built "${ROOT}") 12 BIN="${BUILD_DIR}/linkd" 13 14 T=$(mktemp -d) 15 # Kill the daemon by pid on the way out. `kill %1` does not work here: job 16 # control is off in a non-interactive shell, so a failed test used to leave 17 # the daemon running and holding its port. 18 cleanup() { 19 # `|| true` throughout: the daemon is normally already gone, and a failing 20 # kill under `set -e` would abort the trap and fail an otherwise green run. 21 for f in "${T}/daemon.pid" "${T}/daemon2.pid"; do 22 if [ -f "$f" ]; then kill "$(cat "$f")" 2>/dev/null || true; fi 23 done 24 rm -rf "${T}" || true 25 } 26 trap cleanup EXIT 27 28 DEPINC="${BUILD_DIR}/lib/.dep/include" 29 30 # Harness exposing the internals, so PBKDF2 output can be compared against a 31 # reference implementation rather than only against itself. 32 cat > "${T}/h.c" <<'EOF' 33 #include <stdio.h> 34 #include <stdlib.h> 35 #include <string.h> 36 #include "finwo/pbkdf2.h" 37 #include "util/auth.h" 38 39 static void hex(const unsigned char *b, size_t n) { 40 for (size_t i = 0; i < n; i++) printf("%02x", b[i]); 41 printf("\n"); 42 } 43 44 int main(int argc, char **argv) { 45 if (argc > 1 && !strcmp(argv[1], "kdf")) { 46 /* kdf <pass> <salt-ascii> <iters> -> hex of 32-byte key */ 47 unsigned char out[32]; 48 pbkdf2((const uint8_t*)argv[2], strlen(argv[2]), 49 (const uint8_t*)argv[3], strlen(argv[3]), 50 (unsigned long)atoi(argv[4]), PBKDF2_SHA256, out, sizeof(out)); 51 hex(out, sizeof(out)); 52 return 0; 53 } 54 if (argc > 1 && !strcmp(argv[1], "ab64")) { 55 /* ab64 <ascii> -> encoded, then decoded back as hex */ 56 char enc[256]; 57 unsigned char dec[256]; 58 ab64_encode((const unsigned char*)argv[2], strlen(argv[2]), enc, sizeof(enc)); 59 size_t n = ab64_decode(enc, dec, sizeof(dec)); 60 printf("%s\n", enc); 61 hex(dec, n); 62 return 0; 63 } 64 if (argc > 1 && !strcmp(argv[1], "verify")) { 65 printf("%d\n", auth_verify_hash(argv[2], argv[3])); 66 return 0; 67 } 68 if (argc > 1 && !strcmp(argv[1], "make")) { 69 char *h = auth_make_hash(argv[2], (unsigned)atoi(argv[3])); 70 printf("%s\n", h ? h : "(null)"); 71 return 0; 72 } 73 return 1; 74 } 75 EOF 76 77 cc -O2 -I"${ROOT}/src" -I"${DEPINC}" -o "${T}/h" "${T}/h.c" \ 78 "${ROOT}/src/util/auth.c" \ 79 "${BUILD_DIR}/lib/finwo/pbkdf2/src/pbkdf2.o" \ 80 "${BUILD_DIR}/lib/rxi/log/src/log.o" 2>"${T}/cc.log" || { 81 echo "SKIP: harness did not build"; sed -n '1,5p' "${T}/cc.log"; exit 0; 82 } 83 84 # --- PBKDF2 against OpenSSL ------------------------------------------------ 85 # Self-consistency proves nothing; a wrong HMAC is perfectly self-consistent. 86 if command -v openssl >/dev/null 2>&1; then 87 for vec in "password:salt:1000" "secret:NaCl:4096" "x:0123456789abcdef:29000"; do 88 pass=$(printf '%s' "${vec}" | cut -d: -f1) 89 salt=$(printf '%s' "${vec}" | cut -d: -f2) 90 iter=$(printf '%s' "${vec}" | cut -d: -f3) 91 92 mine=$("${T}/h" kdf "${pass}" "${salt}" "${iter}") 93 ref=$(openssl kdf -keylen 32 -kdfopt digest:SHA256 \ 94 -kdfopt "pass:${pass}" -kdfopt "salt:${salt}" -kdfopt "iter:${iter}" \ 95 PBKDF2 2>/dev/null | tr -d ':' | tr 'A-F' 'a-f') 96 if [ -z "${ref}" ]; then 97 echo "SKIP: openssl kdf unavailable for cross-check" 98 break 99 fi 100 assert_eq "${mine}" "${ref}" "PBKDF2-SHA256 matches OpenSSL (${pass}/${salt}/${iter})" 101 done 102 else 103 echo "SKIP: openssl not available" 104 fi 105 106 # --- adapted base64 -------------------------------------------------------- 107 out=$("${T}/h" ab64 "hello world") 108 enc=$(printf '%s' "${out}" | sed -n 1p) 109 dec=$(printf '%s' "${out}" | sed -n 2p) 110 want=$(printf 'hello world' | od -An -tx1 | tr -d ' \n') 111 assert_eq "${dec}" "${want}" "adapted base64 round-trips" 112 113 case "${enc}" in 114 *=*) echo "FAIL: adapted base64 must not emit padding" >&2; exit 1 ;; 115 *+*) echo "FAIL: adapted base64 must use '.' not '+'" >&2; exit 1 ;; 116 *) echo "PASS: adapted base64 has no padding and no '+'" ;; 117 esac 118 119 # --- hash round-trip ------------------------------------------------------- 120 h=$("${T}/h" make "correct horse" 1000) 121 case "${h}" in 122 '$pbkdf2-sha256$1000$'*) echo "PASS: hash string carries scheme and iterations" ;; 123 *) echo "FAIL: unexpected hash format: ${h}" >&2; exit 1 ;; 124 esac 125 126 assert_eq "$("${T}/h" verify "${h}" "correct horse")" "1" "correct password verifies" 127 assert_eq "$("${T}/h" verify "${h}" "wrong")" "0" "wrong password rejected" 128 assert_eq "$("${T}/h" verify "${h}" "")" "0" "empty password rejected" 129 130 # Two hashes of the same password must differ: the salt has to be random. 131 h2=$("${T}/h" make "correct horse" 1000) 132 if [ "${h}" = "${h2}" ]; then 133 echo "FAIL: identical hashes for the same password; salt is not random" >&2 134 exit 1 135 fi 136 echo "PASS: salt is random across invocations" 137 138 # Tampering with the stored iteration count must not validate. 139 assert_eq "$("${T}/h" verify "\$pbkdf2-sha256\$1\$abc\$def" "correct horse")" "0" \ 140 "malformed hash rejected" 141 142 # --- roles over a live daemon ---------------------------------------------- 143 mkdir -p "${T}/bin" 144 for l in linkd linkctl ifquery; do ln -sf "${BIN}" "${T}/bin/${l}"; done 145 146 RO=$("${T}/h" make "ropass" 1000) 147 FU=$("${T}/h" make "fupass" 1000) 148 printf 'watcher:%s:readonly\nadmin:%s:full\nnorole:%s\n' "${RO}" "${FU}" "${RO}" > "${T}/passwd" 149 chmod 600 "${T}/passwd" 150 151 # No `auto`: nothing is applied, so no privileges are needed. 152 cat > "${T}/interfaces" <<EOF 153 iface lo 154 address 127.0.0.1/8 155 EOF 156 157 cat > "${T}/linkd.cnf" <<EOF 158 config_iface ${T}/interfaces 159 authfile ${T}/passwd 160 listen unix://${T}/linkd.sock 161 listen tcp://127.0.0.1:16797 162 EOF 163 164 cat > "${T}/raw.c" <<'EOF' 165 #include <stdio.h> 166 #include <stdint.h> 167 #include <stdlib.h> 168 #include <string.h> 169 #include <netinet/in.h> 170 #include <arpa/inet.h> 171 #include <sys/socket.h> 172 #include <unistd.h> 173 int main(int argc, char **argv) { 174 (void)argc; 175 char req[8192]; 176 size_t len = fread(req, 1, sizeof(req), stdin); 177 int fd = socket(AF_INET, SOCK_STREAM, 0); 178 struct sockaddr_in a; memset(&a, 0, sizeof(a)); 179 a.sin_family = AF_INET; a.sin_port = htons((uint16_t)atoi(argv[1])); 180 a.sin_addr.s_addr = inet_addr("127.0.0.1"); 181 if (connect(fd, (struct sockaddr*)&a, sizeof(a))) return 1; 182 write(fd, req, len); 183 shutdown(fd, SHUT_WR); 184 char b[8192]; ssize_t n, t = 0; 185 while ((n = read(fd, b+t, sizeof(b)-t-1)) > 0) t += n; 186 b[t] = 0; fwrite(b, 1, t, stdout); 187 return 0; 188 } 189 EOF 190 cc -O2 -o "${T}/raw" "${T}/raw.c" 2>/dev/null || { echo "SKIP: no raw client"; exit 0; } 191 192 OUT="${T}/out" 193 timeout 60 sh -c " 194 '${T}/bin/linkd' --config '${T}/linkd.cnf' --ready-file '${T}/ready' --resync-interval 0 >'${T}/daemon.log' 2>&1 & 195 DPID=\$! 196 echo \$DPID > '${T}/daemon.pid' 197 for i in \$(seq 1 80); do [ -e '${T}/ready' ] && break; sleep 0.25; done 198 199 echo '--badpass--' 200 printf '*3\r\n\$4\r\nAUTH\r\n\$5\r\nadmin\r\n\$5\r\nWRONG\r\n' | '${T}/raw' 16797 201 202 echo '--readonly-query--' 203 printf '*3\r\n\$4\r\nAUTH\r\n\$7\r\nwatcher\r\n\$6\r\nropass\r\n*2\r\n\$7\r\nIFQUERY\r\n\$2\r\nlo\r\n' | '${T}/raw' 16797 | head -3 204 205 echo '--readonly-mutate--' 206 printf '*3\r\n\$4\r\nAUTH\r\n\$7\r\nwatcher\r\n\$6\r\nropass\r\n*2\r\n\$4\r\nIFUP\r\n\$2\r\nlo\r\n' | '${T}/raw' 16797 | tail -1 207 208 echo '--full-mutate--' 209 printf '*3\r\n\$4\r\nAUTH\r\n\$5\r\nadmin\r\n\$6\r\nfupass\r\n*2\r\n\$4\r\nIFUP\r\n\$2\r\nlo\r\n' | '${T}/raw' 16797 | tail -1 210 211 echo '--norole-defaults-readonly--' 212 printf '*3\r\n\$4\r\nAUTH\r\n\$6\r\nnorole\r\n\$6\r\nropass\r\n*2\r\n\$4\r\nIFUP\r\n\$2\r\nlo\r\n' | '${T}/raw' 16797 | tail -1 213 214 echo '--end--' 215 kill \$DPID 2>/dev/null; wait \$DPID 2>/dev/null 216 " > "${OUT}" 2>&1 || true 217 218 section() { sed -n "/^--$1--\$/,/^--/p" "${OUT}" | sed '1d;$d'; } 219 220 assert_contains "$(section badpass)" "WRONGPASS" "wrong password rejected by daemon" 221 assert_contains "$(section readonly-query)" "+OK" "readonly user authenticates" 222 assert_contains "$(section readonly-mutate)" "NOPERM" "readonly user cannot mutate" 223 # Authorization must pass; whether netlink then succeeds depends on privileges. 224 case "$(section full-mutate)" in 225 *NOPERM*|*NOAUTH*) 226 echo "FAIL: full user denied: $(section full-mutate)" >&2; exit 1 ;; 227 *) echo "PASS: full user is authorized to mutate" ;; 228 esac 229 assert_contains "$(section norole-defaults-readonly)" "NOPERM" \ 230 "entry without a role field defaults to readonly" 231 232 # A world-writable password file lets anyone grant themselves access. 233 chmod 666 "${T}/passwd" 234 out=$(timeout 30 sh -c " 235 '${T}/bin/linkd' --config '${T}/linkd.cnf' --ready-file '${T}/ready2' --resync-interval 0 >'${T}/d2.log' 2>&1 & 236 DPID=\$! 237 echo \$DPID > '${T}/daemon2.pid' 238 for i in \$(seq 1 60); do [ -e '${T}/ready2' ] && break; sleep 0.25; done 239 printf '*3\r\n\$4\r\nAUTH\r\n\$5\r\nadmin\r\n\$6\r\nfupass\r\n' | '${T}/raw' 16797 240 kill \$DPID 2>/dev/null 241 wait \$DPID 2>/dev/null 242 " 2>&1 || true) 243 assert_contains "${out}" "WRONGPASS" "world-writable password file is not honoured" 244 assert_file_contains "${T}/d2.log" "world-writable" "world-writable file is reported" 245 246 echo "==> test_auth done"