linkd

Control plane daemon for unos
git clone git://git.finwo.net/app/linkd
Log | Files | Refs | README

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"