summaryrefslogtreecommitdiff
path: root/src/monkeysphere-authentication
blob: cac0e1875ac40a8fe3901ac5ef553e8d2f2cf5e5 (plain)
  1. #!/usr/bin/env bash
  2. # monkeysphere-authentication: Monkeysphere authentication admin tool
  3. #
  4. # The monkeysphere scripts are written by:
  5. # Jameson Rollins <jrollins@fifthhorseman.net>
  6. # Jamie McClelland <jm@mayfirst.org>
  7. # Daniel Kahn Gillmor <dkg@fifthhorseman.net>
  8. #
  9. # They are Copyright 2008, and are all released under the GPL, version 3
  10. # or later.
  11. ########################################################################
  12. PGRM=$(basename $0)
  13. SYSSHAREDIR=${MONKEYSPHERE_SYSSHAREDIR:-"/usr/share/monkeysphere"}
  14. export SYSSHAREDIR
  15. . "${SYSSHAREDIR}/common" || exit 1
  16. SYSDATADIR=${MONKEYSPHERE_SYSDATADIR:-"/var/lib/monkeysphere/authentication"}
  17. export SYSDATADIR
  18. # monkeysphere temp directory, in sysdatadir to enable atomic moves of
  19. # authorized_keys files
  20. MSTMPDIR="${SYSDATADIR}/tmp"
  21. export MSTMPDIR
  22. # UTC date in ISO 8601 format if needed
  23. DATE=$(date -u '+%FT%T')
  24. # unset some environment variables that could screw things up
  25. unset GREP_OPTIONS
  26. # default return code
  27. RETURN=0
  28. ########################################################################
  29. # FUNCTIONS
  30. ########################################################################
  31. usage() {
  32. cat <<EOF >&2
  33. usage: $PGRM <subcommand> [options] [args]
  34. Monkeysphere authentication admin tool.
  35. subcommands:
  36. update-users (u) [USER]... update user authorized_keys files
  37. add-id-certifier (c+) KEYID import and tsign a certification key
  38. --domain (-n) DOMAIN limit ID certifications to DOMAIN
  39. --trust (-t) TRUST trust level of certifier (full)
  40. --depth (-d) DEPTH trust depth for certifier (1)
  41. remove-id-certifier (c-) KEYID remove a certification key
  42. list-id-certifiers (c) list certification keys
  43. expert
  44. diagnostics (d) monkeysphere authentication status
  45. gpg-cmd CMD execute gpg command
  46. version (v) show version number
  47. help (h,?) this help
  48. EOF
  49. }
  50. # function to run command as monkeysphere user
  51. su_monkeysphere_user() {
  52. # if the current user is the monkeysphere user, then just eval
  53. # command
  54. if [ $(id -un) = "$MONKEYSPHERE_USER" ] ; then
  55. eval "$@"
  56. # otherwise su command as monkeysphere user
  57. else
  58. su "$MONKEYSPHERE_USER" -c "$@"
  59. fi
  60. }
  61. # function to interact with the host gnupg keyring
  62. gpg_host() {
  63. local returnCode
  64. GNUPGHOME="$GNUPGHOME_HOST"
  65. export GNUPGHOME
  66. # NOTE: we supress this warning because we need the monkeysphere
  67. # user to be able to read the host pubring. we realize this might
  68. # be problematic, but it's the simplest solution, without too much
  69. # loss of security.
  70. gpg --no-permission-warning "$@"
  71. returnCode="$?"
  72. # always reset the permissions on the host pubring so that the
  73. # monkeysphere user can read the trust signatures
  74. chgrp "$MONKEYSPHERE_USER" "${GNUPGHOME_HOST}/pubring.gpg"
  75. chmod g+r "${GNUPGHOME_HOST}/pubring.gpg"
  76. return "$returnCode"
  77. }
  78. # function to interact with the authentication gnupg keyring
  79. # FIXME: this function requires basically accepts only a single
  80. # argument because of problems with quote expansion. this needs to be
  81. # fixed/improved.
  82. gpg_authentication() {
  83. GNUPGHOME="$GNUPGHOME_AUTHENTICATION"
  84. export GNUPGHOME
  85. su_monkeysphere_user "gpg $@"
  86. }
  87. # check if user is root
  88. is_root() {
  89. [ $(id -u 2>/dev/null) = '0' ]
  90. }
  91. # check that user is root, for functions that require root access
  92. check_user() {
  93. is_root || failure "You must be root to run this command."
  94. }
  95. # output just key fingerprint
  96. fingerprint_server_key() {
  97. # set the pipefail option so functions fails if can't read sec key
  98. set -o pipefail
  99. gpg_host --list-secret-keys --fingerprint \
  100. --with-colons --fixed-list-mode 2> /dev/null | \
  101. grep '^fpr:' | head -1 | cut -d: -f10 2>/dev/null
  102. }
  103. # function to check for host secret key
  104. check_host_keyring() {
  105. fingerprint_server_key >/dev/null \
  106. || failure "You don't appear to have a Monkeysphere host key on this server. Please run 'monkeysphere-server gen-key' first."
  107. }
  108. diagnostics() {
  109. # * check on the status and validity of the key and public certificates
  110. local seckey
  111. local keysfound
  112. local curdate
  113. local warnwindow
  114. local warndate
  115. local create
  116. local expire
  117. local uid
  118. local fingerprint
  119. local badhostkeys
  120. local sshd_config
  121. local problemsfound=0
  122. # FIXME: what's the correct, cross-platform answer?
  123. sshd_config=/etc/ssh/sshd_config
  124. seckey=$(gpg_host --list-secret-keys --fingerprint --with-colons --fixed-list-mode)
  125. keysfound=$(echo "$seckey" | grep -c ^sec:)
  126. curdate=$(date +%s)
  127. # warn when anything is 2 months away from expiration
  128. warnwindow='2 months'
  129. warndate=$(advance_date $warnwindow +%s)
  130. if ! id monkeysphere >/dev/null ; then
  131. echo "! No monkeysphere user found! Please create a monkeysphere system user with bash as its shell."
  132. problemsfound=$(($problemsfound+1))
  133. fi
  134. if ! [ -d "$SYSDATADIR" ] ; then
  135. echo "! no $SYSDATADIR directory found. Please create it."
  136. problemsfound=$(($problemsfound+1))
  137. fi
  138. echo "Checking host GPG key..."
  139. if (( "$keysfound" < 1 )); then
  140. echo "! No host key found."
  141. echo " - Recommendation: run 'monkeysphere-server gen-key'"
  142. problemsfound=$(($problemsfound+1))
  143. elif (( "$keysfound" > 1 )); then
  144. echo "! More than one host key found?"
  145. # FIXME: recommend a way to resolve this
  146. problemsfound=$(($problemsfound+1))
  147. else
  148. create=$(echo "$seckey" | grep ^sec: | cut -f6 -d:)
  149. expire=$(echo "$seckey" | grep ^sec: | cut -f7 -d:)
  150. fingerprint=$(echo "$seckey" | grep ^fpr: | head -n1 | cut -f10 -d:)
  151. # check for key expiration:
  152. if [ "$expire" ]; then
  153. if (( "$expire" < "$curdate" )); then
  154. echo "! Host key is expired."
  155. echo " - Recommendation: extend lifetime of key with 'monkeysphere-server extend-key'"
  156. problemsfound=$(($problemsfound+1))
  157. elif (( "$expire" < "$warndate" )); then
  158. echo "! Host key expires in less than $warnwindow:" $(advance_date $(( $expire - $curdate )) seconds +%F)
  159. echo " - Recommendation: extend lifetime of key with 'monkeysphere-server extend-key'"
  160. problemsfound=$(($problemsfound+1))
  161. fi
  162. fi
  163. # and weirdnesses:
  164. if [ "$create" ] && (( "$create" > "$curdate" )); then
  165. echo "! Host key was created in the future(?!). Is your clock correct?"
  166. echo " - Recommendation: Check clock ($(date +%F_%T)); use NTP?"
  167. problemsfound=$(($problemsfound+1))
  168. fi
  169. # check for UserID expiration:
  170. echo "$seckey" | grep ^uid: | cut -d: -f6,7,10 | \
  171. while IFS=: read create expire uid ; do
  172. # FIXME: should we be doing any checking on the form
  173. # of the User ID? Should we be unmangling it somehow?
  174. if [ "$create" ] && (( "$create" > "$curdate" )); then
  175. echo "! User ID '$uid' was created in the future(?!). Is your clock correct?"
  176. echo " - Recommendation: Check clock ($(date +%F_%T)); use NTP?"
  177. problemsfound=$(($problemsfound+1))
  178. fi
  179. if [ "$expire" ] ; then
  180. if (( "$expire" < "$curdate" )); then
  181. echo "! User ID '$uid' is expired."
  182. # FIXME: recommend a way to resolve this
  183. problemsfound=$(($problemsfound+1))
  184. elif (( "$expire" < "$warndate" )); then
  185. echo "! User ID '$uid' expires in less than $warnwindow:" $(advance_date $(( $expire - $curdate )) seconds +%F)
  186. # FIXME: recommend a way to resolve this
  187. problemsfound=$(($problemsfound+1))
  188. fi
  189. fi
  190. done
  191. # FIXME: verify that the host key is properly published to the
  192. # keyservers (do this with the non-privileged user)
  193. # FIXME: check that there are valid, non-expired certifying signatures
  194. # attached to the host key after fetching from the public keyserver
  195. # (do this with the non-privileged user as well)
  196. # FIXME: propose adding a revoker to the host key if none exist (do we
  197. # have a way to do that after key generation?)
  198. # Ensure that the ssh_host_rsa_key file is present and non-empty:
  199. echo
  200. echo "Checking host SSH key..."
  201. if [ ! -s "${SYSDATADIR}/ssh_host_rsa_key" ] ; then
  202. echo "! The host key as prepared for SSH (${SYSDATADIR}/ssh_host_rsa_key) is missing or empty."
  203. problemsfound=$(($problemsfound+1))
  204. else
  205. if [ $(ls -l "${SYSDATADIR}/ssh_host_rsa_key" | cut -f1 -d\ ) != '-rw-------' ] ; then
  206. echo "! Permissions seem wrong for ${SYSDATADIR}/ssh_host_rsa_key -- should be 0600."
  207. problemsfound=$(($problemsfound+1))
  208. fi
  209. # propose changes needed for sshd_config (if any)
  210. if ! grep -q "^HostKey[[:space:]]\+${SYSDATADIR}/ssh_host_rsa_key$" "$sshd_config"; then
  211. echo "! $sshd_config does not point to the monkeysphere host key (${SYSDATADIR}/ssh_host_rsa_key)."
  212. echo " - Recommendation: add a line to $sshd_config: 'HostKey ${SYSDATADIR}/ssh_host_rsa_key'"
  213. problemsfound=$(($problemsfound+1))
  214. fi
  215. if badhostkeys=$(grep -i '^HostKey' "$sshd_config" | grep -v "^HostKey[[:space:]]\+${SYSDATADIR}/ssh_host_rsa_key$") ; then
  216. echo "! $sshd_config refers to some non-monkeysphere host keys:"
  217. echo "$badhostkeys"
  218. echo " - Recommendation: remove the above HostKey lines from $sshd_config"
  219. problemsfound=$(($problemsfound+1))
  220. fi
  221. # FIXME: test (with ssh-keyscan?) that the running ssh
  222. # daemon is actually offering the monkeysphere host key.
  223. fi
  224. fi
  225. # FIXME: look at the ownership/privileges of the various keyrings,
  226. # directories housing them, etc (what should those values be? can
  227. # we make them as minimal as possible?)
  228. # FIXME: look to see that the ownertrust rules are set properly on the
  229. # authentication keyring
  230. # FIXME: make sure that at least one identity certifier exists
  231. # FIXME: look at the timestamps on the monkeysphere-generated
  232. # authorized_keys files -- warn if they seem out-of-date.
  233. # FIXME: check for a cronjob that updates monkeysphere-generated
  234. # authorized_keys?
  235. echo
  236. echo "Checking for MonkeySphere-enabled public-key authentication for users ..."
  237. # Ensure that User ID authentication is enabled:
  238. if ! grep -q "^AuthorizedKeysFile[[:space:]]\+${SYSDATADIR}/authorized_keys/%u$" "$sshd_config"; then
  239. echo "! $sshd_config does not point to monkeysphere authorized keys."
  240. echo " - Recommendation: add a line to $sshd_config: 'AuthorizedKeysFile ${SYSDATADIR}/authorized_keys/%u'"
  241. problemsfound=$(($problemsfound+1))
  242. fi
  243. if badauthorizedkeys=$(grep -i '^AuthorizedKeysFile' "$sshd_config" | grep -v "^AuthorizedKeysFile[[:space:]]\+${SYSDATADIR}/authorized_keys/%u$") ; then
  244. echo "! $sshd_config refers to non-monkeysphere authorized_keys files:"
  245. echo "$badauthorizedkeys"
  246. echo " - Recommendation: remove the above AuthorizedKeysFile lines from $sshd_config"
  247. problemsfound=$(($problemsfound+1))
  248. fi
  249. if [ "$problemsfound" -gt 0 ]; then
  250. echo "When the above $problemsfound issue"$(if [ "$problemsfound" -eq 1 ] ; then echo " is" ; else echo "s are" ; fi)" resolved, please re-run:"
  251. echo " monkeysphere-server diagnostics"
  252. else
  253. echo "Everything seems to be in order!"
  254. fi
  255. }
  256. # retrieve key from web of trust, import it into the host keyring, and
  257. # ltsign the key in the host keyring so that it may certify other keys
  258. add_certifier() {
  259. local domain
  260. local trust
  261. local depth
  262. local keyID
  263. local fingerprint
  264. local ltsignCommand
  265. local trustval
  266. # set default values for trust depth and domain
  267. domain=
  268. trust=full
  269. depth=1
  270. # get options
  271. while true ; do
  272. case "$1" in
  273. -n|--domain)
  274. domain="$2"
  275. shift 2
  276. ;;
  277. -t|--trust)
  278. trust="$2"
  279. shift 2
  280. ;;
  281. -d|--depth)
  282. depth="$2"
  283. shift 2
  284. ;;
  285. *)
  286. if [ "$(echo "$1" | cut -c 1)" = '-' ] ; then
  287. failure "Unknown option '$1'.
  288. Type '$PGRM help' for usage."
  289. fi
  290. break
  291. ;;
  292. esac
  293. done
  294. keyID="$1"
  295. if [ -z "$keyID" ] ; then
  296. failure "You must specify the key ID of a key to add, or specify a file to read the key from."
  297. fi
  298. if [ -f "$keyID" ] ; then
  299. echo "Reading key from file '$keyID':"
  300. importinfo=$(gpg_authentication "--import" < "$keyID" 2>&1) || failure "could not read key from '$keyID'"
  301. # FIXME: if this is tried when the key database is not
  302. # up-to-date, i got these errors (using set -x):
  303. # ++ su -m monkeysphere -c '\''gpg --import'\''
  304. # Warning: using insecure memory!
  305. # gpg: key D21739E9: public key "Daniel Kahn Gillmor <dkg@fifthhorseman.net>" imported
  306. # gpg: Total number processed: 1
  307. # gpg: imported: 1 (RSA: 1)
  308. # gpg: can'\''t create `/var/monkeysphere/gnupg-host/pubring.gpg.tmp'\'': Permission denied
  309. # gpg: failed to rebuild keyring cache: Permission denied
  310. # gpg: 3 marginal(s) needed, 1 complete(s) needed, PGP trust model
  311. # gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u
  312. # gpg: next trustdb check due at 2009-01-10'
  313. # + failure 'could not read key from '\''/root/dkg.gpg'\'''
  314. # + echo 'could not read key from '\''/root/dkg.gpg'\'''
  315. keyID=$(echo "$importinfo" | grep '^gpg: key ' | cut -f2 -d: | cut -f3 -d\ )
  316. if [ -z "$keyID" ] || [ $(echo "$keyID" | wc -l) -ne 1 ] ; then
  317. failure "Expected there to be a single gpg key in the file."
  318. fi
  319. else
  320. # get the key from the key server
  321. gpg_authentication "--keyserver $KEYSERVER --recv-key '0x${keyID}!'" || failure "Could not receive a key with this ID from the '$KEYSERVER' keyserver."
  322. fi
  323. export keyID
  324. # get the full fingerprint of a key ID
  325. fingerprint=$(gpg_authentication "--list-key --with-colons --with-fingerprint 0x${keyID}!" | \
  326. grep '^fpr:' | grep "$keyID" | cut -d: -f10)
  327. if [ -z "$fingerprint" ] ; then
  328. failure "Key '$keyID' not found."
  329. fi
  330. echo
  331. echo "key found:"
  332. gpg_authentication "--fingerprint 0x${fingerprint}!"
  333. echo "Are you sure you want to add the above key as a"
  334. read -p "certifier of users on this system? (y/N) " OK; OK=${OK:-N}
  335. if [ "${OK/y/Y}" != 'Y' ] ; then
  336. failure "Identity certifier not added."
  337. fi
  338. # export the key to the host keyring
  339. gpg_authentication "--export 0x${fingerprint}!" | gpg_host --import
  340. if [ "$trust" = marginal ]; then
  341. trustval=1
  342. elif [ "$trust" = full ]; then
  343. trustval=2
  344. else
  345. failure "Trust value requested ('$trust') was unclear (only 'marginal' or 'full' are supported)."
  346. fi
  347. # ltsign command
  348. # NOTE: *all* user IDs will be ltsigned
  349. ltsignCommand=$(cat <<EOF
  350. ltsign
  351. y
  352. $trustval
  353. $depth
  354. $domain
  355. y
  356. save
  357. EOF
  358. )
  359. # ltsign the key
  360. if echo "$ltsignCommand" | \
  361. gpg_host --quiet --command-fd 0 --edit-key "0x${fingerprint}!" ; then
  362. # update the trustdb for the authentication keyring
  363. gpg_authentication "--check-trustdb"
  364. echo
  365. echo "Identity certifier added."
  366. else
  367. failure "Problem adding identify certifier."
  368. fi
  369. }
  370. # delete a certifiers key from the host keyring
  371. remove_certifier() {
  372. local keyID
  373. local fingerprint
  374. keyID="$1"
  375. if [ -z "$keyID" ] ; then
  376. failure "You must specify the key ID of a key to remove."
  377. fi
  378. if gpg_authentication "--no-options --list-options show-uid-validity --keyring ${GNUPGHOME_AUTHENTICATION}/pubring.gpg --list-key 0x${keyID}!" ; then
  379. read -p "Really remove above listed identity certifier? (y/N) " OK; OK=${OK:-N}
  380. if [ "${OK/y/Y}" != 'Y' ] ; then
  381. failure "Identity certifier not removed."
  382. fi
  383. else
  384. failure
  385. fi
  386. # delete the requested key
  387. if gpg_authentication "--delete-key --batch --yes 0x${keyID}!" ; then
  388. # delete key from host keyring as well
  389. gpg_host --delete-key --batch --yes "0x${keyID}!"
  390. # update the trustdb for the authentication keyring
  391. gpg_authentication "--check-trustdb"
  392. echo
  393. echo "Identity certifier removed."
  394. else
  395. failure "Problem removing identity certifier."
  396. fi
  397. }
  398. # list the host certifiers
  399. list_certifiers() {
  400. local keys
  401. local key
  402. # find trusted keys in authentication keychain
  403. keys=$(gpg_authentication "--no-options --list-options show-uid-validity --keyring ${GNUPGHOME_AUTHENTICATION}/pubring.gpg --list-keys --with-colons --fingerprint" | \
  404. grep ^pub: | cut -d: -f2,5 | egrep '^(u|f):' | cut -d: -f2)
  405. # output keys
  406. for key in $keys ; do
  407. gpg_authentication "--no-options --list-options show-uid-validity --keyring ${GNUPGHOME_AUTHENTICATION}/pubring.gpg --list-key --fingerprint $key"
  408. done
  409. }
  410. ########################################################################
  411. # MAIN
  412. ########################################################################
  413. # unset variables that should be defined only in config file
  414. unset KEYSERVER
  415. unset AUTHORIZED_USER_IDS
  416. unset RAW_AUTHORIZED_KEYS
  417. unset MONKEYSPHERE_USER
  418. # load configuration file
  419. [ -e ${MONKEYSPHERE_SERVER_CONFIG:="${SYSCONFIGDIR}/monkeysphere-server.conf"} ] && . "$MONKEYSPHERE_SERVER_CONFIG"
  420. # set empty config variable with ones from the environment, or with
  421. # defaults
  422. LOG_LEVEL=${MONKEYSPHERE_LOG_LEVEL:=${LOG_LEVEL:="INFO"}}
  423. KEYSERVER=${MONKEYSPHERE_KEYSERVER:=${KEYSERVER:="pool.sks-keyservers.net"}}
  424. AUTHORIZED_USER_IDS=${MONKEYSPHERE_AUTHORIZED_USER_IDS:=${AUTHORIZED_USER_IDS:="%h/.monkeysphere/authorized_user_ids"}}
  425. RAW_AUTHORIZED_KEYS=${MONKEYSPHERE_RAW_AUTHORIZED_KEYS:=${RAW_AUTHORIZED_KEYS:="%h/.ssh/authorized_keys"}}
  426. MONKEYSPHERE_USER=${MONKEYSPHERE_MONKEYSPHERE_USER:=${MONKEYSPHERE_USER:="monkeysphere"}}
  427. # other variables
  428. CHECK_KEYSERVER=${MONKEYSPHERE_CHECK_KEYSERVER:="true"}
  429. REQUIRED_USER_KEY_CAPABILITY=${MONKEYSPHERE_REQUIRED_USER_KEY_CAPABILITY:="a"}
  430. GNUPGHOME_HOST=${MONKEYSPHERE_GNUPGHOME_HOST:="${SYSDATADIR}/gnupg-host"}
  431. GNUPGHOME_AUTHENTICATION=${MONKEYSPHERE_GNUPGHOME_AUTHENTICATION:="${SYSDATADIR}/gnupg-authentication"}
  432. # export variables needed in su invocation
  433. export DATE
  434. export MODE
  435. export MONKEYSPHERE_USER
  436. export LOG_LEVEL
  437. export KEYSERVER
  438. export CHECK_KEYSERVER
  439. export REQUIRED_USER_KEY_CAPABILITY
  440. export GNUPGHOME_HOST
  441. export GNUPGHOME_AUTHENTICATION
  442. export GNUPGHOME
  443. # get subcommand
  444. COMMAND="$1"
  445. [ "$COMMAND" ] || failure "Type '$PGRM help' for usage."
  446. shift
  447. case $COMMAND in
  448. 'update-users'|'update-user'|'u')
  449. check_user
  450. check_host_keyring
  451. update_users "$@"
  452. ;;
  453. 'add-identity-certifier'|'add-id-certifier'|'add-certifier'|'c+')
  454. check_user
  455. check_host_keyring
  456. add_certifier "$@"
  457. ;;
  458. 'remove-identity-certifier'|'remove-id-certifier'|'remove-certifier'|'c-')
  459. check_user
  460. check_host_keyring
  461. remove_certifier "$@"
  462. ;;
  463. 'list-identity-certifiers'|'list-id-certifiers'|'list-certifiers'|'list-certifier'|'c')
  464. check_user
  465. check_host_keyring
  466. list_certifiers "$@"
  467. ;;
  468. 'expert'|'e')
  469. check_user
  470. SUBCOMMAND="$1"
  471. shift
  472. case "$SUBCOMMAND" in
  473. 'diagnostics'|'d')
  474. diagnostics
  475. ;;
  476. 'gpg-cmd')
  477. gpg_authentication "$@"
  478. ;;
  479. *)
  480. failure "Unknown expert subcommand: '$COMMAND'
  481. Type '$PGRM help' for usage."
  482. ;;
  483. esac
  484. ;;
  485. 'version'|'v')
  486. echo "$VERSION"
  487. ;;
  488. '--help'|'help'|'-h'|'h'|'?')
  489. usage
  490. ;;
  491. *)
  492. failure "Unknown command: '$COMMAND'
  493. Type '$PGRM help' for usage."
  494. ;;
  495. esac
  496. exit "$RETURN"