Shellpki is a very tiny and easy PKI in command lines.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

498 lines
15 KiB

  1. #!/bin/sh
  2. #
  3. # shellpki is a wrapper around openssl to manage a small PKI
  4. #
  5. set -e
  6. init() {
  7. umask 0177
  8. [ -d "${CADIR}" ] || mkdir -m 0750 "${CADIR}"
  9. [ -d "${CRTDIR}" ] || mkdir -m 0750 "${CRTDIR}"
  10. [ -f "${INDEX}" ] || touch "${INDEX}"
  11. [ -f "${CRL}" ] || touch "${CRL}"
  12. [ -f "${SERIAL}" ] || echo "01" > "${SERIAL}"
  13. cn="${1:-}"
  14. [ -z "${cn}" ] && usage >&2 && exit 1
  15. if [ -f "${CAKEY}" ]; then
  16. printf "%s already exists, do you really want to erase it ? [y/N] " "${CAKEY}"
  17. read -r REPLY
  18. resp=$(echo "${REPLY}"|tr 'Y' 'y')
  19. [ "${resp}" = "y" ] && rm -f "${CAKEY}" "${CACERT}"
  20. fi
  21. [ ! -f "${CAKEY}" ] && "$OPENSSL" \
  22. genrsa \
  23. -out "${CAKEY}" \
  24. -aes256 4096 >/dev/null 2>&1
  25. if [ -f "${CACERT}" ]; then
  26. printf "%s already exists, do you really want to erase it ? [y/N] " "${CACERT}"
  27. read -r REPLY
  28. resp=$(echo "${REPLY}"|tr 'Y' 'y')
  29. [ "${resp}" = "y" ] && rm "${CACERT}"
  30. fi
  31. [ ! -f "${CACERT}" ] && ask_ca_password 0
  32. [ ! -f "${CACERT}" ] && CA_PASSWORD="${CA_PASSWORD}" "${OPENSSL}" \
  33. req -new \
  34. -batch -sha512 \
  35. -x509 -days 3650 \
  36. -extensions v3_ca \
  37. -key "${CAKEY}" \
  38. -out "${CACERT}" \
  39. -passin env:CA_PASSWORD \
  40. -config /dev/stdin <<EOF
  41. $(cat "${CONFFILE}")
  42. commonName_default = ${cn}
  43. EOF
  44. }
  45. ocsp() {
  46. umask 0177
  47. ocsp_uri="${1:-}"
  48. [ -z "${ocsp_uri}" ] && usage >&2 && exit 1
  49. url=$(echo "${ocsp_uri}"|cut -d':' -f1)
  50. port=$(echo "${ocsp_uri}"|cut -d':' -f2)
  51. [ ! -f "${OCSPKEY}" ] && "$OPENSSL" \
  52. genrsa \
  53. -out "${OCSPKEY}" \
  54. 2048 >/dev/null 2>&1
  55. "$OPENSSL" req \
  56. -batch -new \
  57. -key "${OCSPKEY}" \
  58. -out "${CSRDIR}/ocsp.csr" \
  59. -config /dev/stdin <<EOF
  60. $(cat "${CONFFILE}")
  61. commonName_default = ${url}
  62. [ usr_cert ]
  63. authorityInfoAccess = OCSP;URI:http://${ocsp_uri}
  64. EOF
  65. [ ! -f "${OCSPCERT}" ] && ask_ca_password 0
  66. [ ! -f "${OCSPCERT}" ] && CA_PASSWORD="${CA_PASSWORD}" "${OPENSSL}" \
  67. ca \
  68. -extensions v3_ocsp \
  69. -in "${CSRDIR}/ocsp.csr" \
  70. -out "${OCSPCERT}" \
  71. -passin env:CA_PASSWORD \
  72. -config "${CONFFILE}"
  73. exec "${OPENSSL}" ocsp -ignore_err -index "${INDEX}" -port "${port}" -rsigner "${OCSPCERT}" -rkey "${OCSPKEY}" -CA "${CACERT}" -text
  74. }
  75. usage() {
  76. cat <<EOF
  77. Usage: ${0} <subcommand> [options] [CommonName]
  78. Initialize PKI (create CA key and self-signed cert) :
  79. ${0} init <commonName_for_CA>
  80. Run OCSPD server :
  81. ${0} ocsp <ocsp_uri:ocsp_port>
  82. Create a client cert with key and CSR directly generated on server
  83. (use -p for set a password on client key) :
  84. ${0} create [-p] <commonName>
  85. Create a client cert from a CSR (doesn't need key) :
  86. ${0} create -f <path>
  87. Revoke a client cert with is commonName (CN) :
  88. ${0} revoke <commonName>
  89. List all actually valid commonName (CN) :
  90. ${0} list [-a|v|r]
  91. Check expiration date of valid certificates :
  92. ${0} check
  93. EOF
  94. }
  95. error() {
  96. echo "${1}" >&2
  97. exit 1
  98. }
  99. warning() {
  100. echo "${1}" >&2
  101. }
  102. ask_ca_password() {
  103. [ ! -f "${CAKEY}" ] && error "You must initialize your's PKI with shellpki init !"
  104. attempt=$((${1} + 1))
  105. [ "${attempt}" -gt 1 ] && warning "Invalid password, retry."
  106. trap 'unset CA_PASSWORD' 0
  107. stty -echo
  108. printf "Password for CA key : "
  109. read -r CA_PASSWORD
  110. stty echo
  111. printf "\n"
  112. [ "${CA_PASSWORD}" != "" ] || ask_ca_password "${attempt}"
  113. CA_PASSWORD="${CA_PASSWORD}" "${OPENSSL}" rsa \
  114. -in "${CAKEY}" \
  115. -passin env:CA_PASSWORD \
  116. >/dev/null 2>&1 \
  117. || ask_ca_password "${attempt}"
  118. }
  119. create() {
  120. from_csr=1
  121. with_pass=1
  122. while :; do
  123. case "${1}" in
  124. -f|--file)
  125. shift
  126. [ ! -f "${1}" ] && error "${1} must be a file"
  127. from_csr=0
  128. csr_file=$(readlink -f "${1}")
  129. shift;;
  130. -p|--password)
  131. with_pass=0
  132. shift;;
  133. --)
  134. shift
  135. break;;
  136. -?*)
  137. warning "unknow option ${1} (ignored)"
  138. shift;;
  139. *)
  140. break;;
  141. esac
  142. done
  143. cn="${1:-}"
  144. if [ "${from_csr}" -eq 0 ]; then
  145. [ "${with_pass}" -eq 0 ] && warning "Warning: -p made nothing with -f"
  146. # ask for CA passphrase
  147. ask_ca_password 0
  148. # check if csr_file is a CSR
  149. "${OPENSSL}" req \
  150. -noout -subject \
  151. -in "${csr_file}" \
  152. >/dev/null 2>&1 \
  153. || error "${csr_file} is not a valid CSR !"
  154. # check if csr_file contain a CN
  155. "${OPENSSL}" req \
  156. -noout -subject \
  157. -in "${csr_file}" \
  158. | grep -Eo "CN\s*=[^,/]*" \
  159. >/dev/null 2>&1 \
  160. || error "${csr_file} don't contain a CommonName !"
  161. # get CN from CSR
  162. cn=$("${OPENSSL}" req -noout -subject -in "${csr_file}"|grep -Eo "CN\s*=[^,/]*"|cut -d'=' -f2|xargs)
  163. # check if CN already exist
  164. [ -f "${CRTDIR}/${cn}.crt" ] && error "${cn} already used !"
  165. # ca sign and generate cert
  166. CA_PASSWORD="${CA_PASSWORD}" "${OPENSSL}" ca \
  167. -config "${CONFFILE}" \
  168. -in "${csr_file}" \
  169. -passin env:CA_PASSWORD \
  170. -out "${CRTDIR}/${cn}.crt"
  171. echo "The CRT file is available in ${CRTDIR}/${cn}.crt"
  172. else
  173. [ -z "${cn}" ] && usage >&2 && exit 1
  174. # check if CN already exist
  175. [ -f "${CRTDIR}/${cn}.crt" ] && error "${cn} already used !"
  176. # ask for client key passphrase
  177. if [ "${with_pass}" -eq 0 ]; then
  178. trap 'unset PASSWORD' 0
  179. stty -echo
  180. printf "Password for user key : "
  181. read -r PASSWORD
  182. stty echo
  183. printf "\n"
  184. fi
  185. # ask for CA passphrase
  186. ask_ca_password 0
  187. # generate private key
  188. if [ "${with_pass}" -eq 0 ]; then
  189. PASSWORD="${PASSWORD}" "$OPENSSL" genrsa \
  190. -aes256 -passout env:PASSWORD \
  191. -out "${KEYDIR}/${cn}-${TIMESTAMP}.key" \
  192. 2048 >/dev/null 2>&1
  193. else
  194. "$OPENSSL" genrsa \
  195. -out "${KEYDIR}/${cn}-${TIMESTAMP}.key" \
  196. 2048 >/dev/null 2>&1
  197. fi
  198. if [ "${with_pass}" -eq 0 ]; then
  199. # generate csr req
  200. PASSWORD="${PASSWORD}" "$OPENSSL" req \
  201. -batch -new \
  202. -key "${KEYDIR}/${cn}-${TIMESTAMP}.key" \
  203. -passin env:PASSWORD \
  204. -out "${CSRDIR}/${cn}-${TIMESTAMP}.csr" \
  205. -config /dev/stdin <<EOF
  206. $(cat "${CONFFILE}")
  207. commonName_default = ${cn}
  208. EOF
  209. else
  210. # generate csr req
  211. "$OPENSSL" req \
  212. -batch -new \
  213. -key "${KEYDIR}/${cn}-${TIMESTAMP}.key" \
  214. -out "${CSRDIR}/${cn}-${TIMESTAMP}.csr" \
  215. -config /dev/stdin <<EOF
  216. $(cat "${CONFFILE}")
  217. commonName_default = ${cn}
  218. EOF
  219. fi
  220. # ca sign and generate cert
  221. CA_PASSWORD="${CA_PASSWORD}" "${OPENSSL}" ca \
  222. -config "${CONFFILE}" \
  223. -passin env:CA_PASSWORD \
  224. -in "${CSRDIR}/${cn}-${TIMESTAMP}.csr" \
  225. -out "${CRTDIR}/${cn}.crt"
  226. # check if CRT is a valid
  227. "${OPENSSL}" x509 \
  228. -noout -subject \
  229. -in "${CRTDIR}/${cn}.crt" \
  230. >/dev/null 2>&1 \
  231. || rm -f "${CRTDIR}/${cn}.crt"
  232. [ -f "${CRTDIR}/${cn}.crt" ] || error "Error in CSR creation"
  233. chmod 640 "${CRTDIR}/${cn}.crt"
  234. echo "The CRT file is available in ${CRTDIR}/${cn}.crt"
  235. # generate pkcs12 format
  236. if [ "${with_pass}" -eq 0 ]; then
  237. PASSWORD="${PASSWORD}" "${OPENSSL}" pkcs12 -export -nodes -passin env:PASSWORD -passout env:PASSWORD -inkey "${KEYDIR}/${cn}-${TIMESTAMP}.key" -in "${CRTDIR}/${cn}.crt" -out "${PKCS12DIR}/${cn}-${TIMESTAMP}.p12"
  238. else
  239. "${OPENSSL}" pkcs12 -export -nodes -passout pass: -inkey "${KEYDIR}/${cn}-${TIMESTAMP}.key" -in "${CRTDIR}/${cn}.crt" -out "${PKCS12DIR}/${cn}-${TIMESTAMP}.p12"
  240. fi
  241. chmod 640 "${PKCS12DIR}/${cn}-${TIMESTAMP}.p12"
  242. echo "The PKCS12 config file is available in ${PKCS12DIR}/${cn}-${TIMESTAMP}.p12"
  243. # generate openvpn format
  244. if [ -e "${CADIR}/ovpn.conf" ]; then
  245. cat "${CADIR}/ovpn.conf" - > "${OVPNDIR}/${cn}-${TIMESTAMP}.ovpn" <<EOF
  246. <ca>
  247. $(cat "${CACERT}")
  248. </ca>
  249. <cert>
  250. $(cat "${CRTDIR}/${cn}.crt")
  251. </cert>
  252. <key>
  253. $(cat "${KEYDIR}/${cn}-${TIMESTAMP}.key")
  254. </key>
  255. EOF
  256. chmod 640 "${OVPNDIR}/${cn}-${TIMESTAMP}.ovpn"
  257. echo "The OpenVPN config file is available in ${OVPNDIR}/${cn}-${TIMESTAMP}.ovpn"
  258. fi
  259. fi
  260. }
  261. revoke() {
  262. [ "${1}" = "" ] && usage >&2 && exit 1
  263. # get CN from param
  264. cn="${1}"
  265. # check if CRT exists
  266. [ ! -f "${CRTDIR}/${cn}.crt" ] && error "Unknow CN : ${cn}"
  267. # check if CRT is a valid
  268. "${OPENSSL}" x509 -noout -subject -in "${CRTDIR}/${cn}.crt" >/dev/null 2>&1 || error "${CRTDIR}/${cn}.crt is not a valid CRT, you msust delete it !"
  269. # ask for CA passphrase
  270. ask_ca_password 0
  271. echo "Revoke certificate ${CRTDIR}/${cn}.crt :"
  272. CA_PASSWORD="${CA_PASSWORD}" "$OPENSSL" ca \
  273. -config "${CONFFILE}" \
  274. -passin env:CA_PASSWORD \
  275. -revoke "${CRTDIR}/${cn}.crt" \
  276. && rm "${CRTDIR}/${cn}.crt"
  277. CA_PASSWORD="${CA_PASSWORD}" "$OPENSSL" ca \
  278. -config "${CONFFILE}" \
  279. -passin env:CA_PASSWORD \
  280. -gencrl -out "${CRL}"
  281. }
  282. list() {
  283. [ -f "${INDEX}" ] || exit 0
  284. list_valid=0
  285. list_revoked=1
  286. while :; do
  287. case "${1}" in
  288. -a|--all)
  289. list_valid=0
  290. list_revoked=0
  291. shift;;
  292. -v|--valid)
  293. list_valid=0
  294. list_revoked=1
  295. shift;;
  296. -r|--revoked)
  297. list_valid=1
  298. list_revoked=0
  299. shift;;
  300. --)
  301. shift
  302. break;;
  303. -?*)
  304. warning "unknow option ${1} (ignored)"
  305. shift;;
  306. *)
  307. break;;
  308. esac
  309. done
  310. [ "${list_valid}" -eq 0 ] && certs=$(grep "^V" "${INDEX}")
  311. [ "${list_revoked}" -eq 0 ] && certs=$(grep "^R" "${INDEX}")
  312. [ "${list_valid}" -eq 0 ] && [ "${list_revoked}" -eq 0 ] && certs=$(cat "${INDEX}")
  313. echo "${certs}" | grep -Eo "CN\s*=[^,/]*" | cut -d'=' -f2 | xargs -n1
  314. }
  315. check() {
  316. # default expiration alert
  317. # TODO : permit override with parameters
  318. min_day=90
  319. cur_epoch=$(date -u +'%s')
  320. for cert in ${CRTDIR}/*; do
  321. end_date=$(openssl x509 -noout -enddate -in "${cert}" | cut -d'=' -f2)
  322. end_epoch=$(date -ud "${end_date}" +'%s')
  323. diff_epoch=$((end_epoch - cur_epoch))
  324. diff_day=$((diff_epoch/60/60/24))
  325. if [ "${diff_day}" -lt "${min_day}" ]; then
  326. if [ "${diff_day}" -le 0 ]; then
  327. echo "${cert} has expired"
  328. else
  329. echo "${cert} expire in ${diff_day} days"
  330. fi
  331. fi
  332. done
  333. }
  334. main() {
  335. # default config
  336. # TODO : override with /etc/default/shellpki
  337. CONFFILE="/etc/shellpki/openssl.cnf"
  338. PKIUSER="shellpki"
  339. [ "$(uname)" = "OpenBSD" ] && PKIUSER="_shellpki"
  340. [ "${USER}" != "root" ] || [ "${USER}" != "${PKIUSER}" ] || error "Please become root before running ${0} !"
  341. # retrieve CA path from config file
  342. CADIR=$(grep -E "^dir" "${CONFFILE}" | cut -d'=' -f2|xargs -n1)
  343. CAKEY=$(grep -E "^private_key" "${CONFFILE}" | cut -d'=' -f2|xargs -n1|sed "s~\$dir~${CADIR}~")
  344. CACERT=$(grep -E "^certificate" "${CONFFILE}" | cut -d'=' -f2|xargs -n1|sed "s~\$dir~${CADIR}~")
  345. OCSPKEY="${CADIR}/ocsp.key"
  346. OCSPCERT="${CADIR}/ocsp.pem"
  347. CRTDIR=$(grep -E "^certs" "${CONFFILE}" | cut -d'=' -f2|xargs -n1|sed "s~\$dir~${CADIR}~")
  348. TMPDIR=$(grep -E "^new_certs_dir" "${CONFFILE}" | cut -d'=' -f2|xargs -n1|sed "s~\$dir~${CADIR}~")
  349. INDEX=$(grep -E "^database" "${CONFFILE}" | cut -d'=' -f2|xargs -n1|sed "s~\$dir~${CADIR}~")
  350. SERIAL=$(grep -E "^serial" "${CONFFILE}" | cut -d'=' -f2|xargs -n1|sed "s~\$dir~${CADIR}~")
  351. CRL=$(grep -E "^crl" "${CONFFILE}" | cut -d'=' -f2|xargs -n1|sed "s~\$dir~${CADIR}~")
  352. # directories for clients key, csr, crt
  353. KEYDIR="${CADIR}/private"
  354. CSRDIR="${CADIR}/requests"
  355. PKCS12DIR="${CADIR}/pkcs12"
  356. OVPNDIR="${CADIR}/openvpn"
  357. OPENSSL=$(command -v openssl)
  358. TIMESTAMP=$(/bin/date +"%s")
  359. if ! getent passwd "${PKIUSER}" >/dev/null || ! getent group "${PKIUSER}" >/dev/null; then
  360. error "You must create ${PKIUSER} user and group !"
  361. fi
  362. [ -e "${CONFFILE}" ] || error "${CONFFILE} is missing"
  363. mkdir -p "${CADIR}" "${CRTDIR}" "${KEYDIR}" "${CSRDIR}" "${PKCS12DIR}" "${OVPNDIR}" "${TMPDIR}"
  364. command=${1:-help}
  365. case "${command}" in
  366. init)
  367. shift
  368. init "$@"
  369. ;;
  370. ocsp)
  371. shift
  372. ocsp "$@"
  373. ;;
  374. create)
  375. shift
  376. create "$@"
  377. ;;
  378. revoke)
  379. shift
  380. revoke "$@"
  381. ;;
  382. list)
  383. shift
  384. list "$@"
  385. ;;
  386. check)
  387. shift
  388. check "$@"
  389. ;;
  390. *)
  391. usage >&2
  392. exit 1
  393. ;;
  394. esac
  395. # fix right
  396. chown -R "${PKIUSER}":"${PKIUSER}" "${CADIR}"
  397. chmod 750 "${CADIR}" "${CRTDIR}" "${KEYDIR}" "${CSRDIR}" "${PKCS12DIR}" "${OVPNDIR}" "${TMPDIR}"
  398. chmod 600 "${INDEX}"* "${SERIAL}"* "${CAKEY}" "${CRL}"
  399. chmod 640 "${CACERT}"
  400. }
  401. main "$@"