Browse Source

uacme: add package

lightweight client for the RFC8555 ACMEv2 protocol, written in plain C code
with minimal dependencies (libcurl and one of GnuTLS, OpenSSL or mbedTLS).

Signed-off-by: Lucian Cristian <lucian.cristian@gmail.com>
lilik-openwrt-22.03
Lucian Cristian 5 years ago
parent
commit
cb81ac1e06
4 changed files with 536 additions and 0 deletions
  1. +93
    -0
      net/uacme/Makefile
  2. +14
    -0
      net/uacme/files/acme.config
  3. +35
    -0
      net/uacme/files/acme.init
  4. +394
    -0
      net/uacme/files/run.sh

+ 93
- 0
net/uacme/Makefile View File

@ -0,0 +1,93 @@
#
# Copyright (C) 2019 Lucian Cristian <lucian.cristian@gmail.com>
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=uacme
PKG_VERSION:=1.0.20
PKG_RELEASE:=1
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/ndilieto/uacme/tar.gz/upstream/$(PKG_VERSION)?
PKG_HASH:=c9106e166156685fcf9f164f0b2935cf680251a653f460b04da26a290e946bf9
PKG_MAINTAINER:=Lucian Cristian <lucian.cristian@gmail.com>
PKG_LICENSE:=GPL-3.0-or-later
PKG_LICENSE_FILES:=COPYING
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-upstream-$(PKG_VERSION)
PKG_INSTALL:=1
PKG_BUILD_PARALLEL:=1
PKG_CONFIG_DEPENDS:= \
CONFIG_LIBCURL_GNUTLS \
CONFIG_LIBCURL_MBEDTLS \
CONFIG_LIBCURL_OPENSSL \
CONFIG_LIBCURL_WOLFSSL \
CONFIG_LIBCURL_NOSSL
include $(INCLUDE_DIR)/package.mk
define Package/uacme
SECTION:=net
CATEGORY:=Network
DEPENDS:=+libcurl +LIBCURL_WOLFSSL:libmbedtls
TITLE:=lightweight client for ACMEv2
URL:=https://github.com/ndilieto/uacme
endef
define Package/uacme/Default/description
lightweight client for the RFC8555 ACMEv2 protocol, written in plain C code
with minimal dependencies (libcurl and one of GnuTLS, OpenSSL or mbedTLS).
The ACMEv2 protocol allows a Certificate Authority (https://letsencrypt.org
is a popular one) and an applicant to automate the process of verification
and certificate issuance. The protocol also provides facilities for other
certificate management functions, such as certificate revocation.
endef
define Package/uacme/config
if PACKAGE_uacme && LIBCURL_WOLFSSL
comment "libcurl uses WolfSSL; uacme will install mbedtls"
comment "Choose another SSL lib in libcurl to avoid this"
endif
if PACKAGE_uacme && LIBCURL_NOSSL
comment "libcurl is built without ssl; uacme needs ssl support"
comment "Choose another SSL lib in libcurl to avoid this"
endif
endef
TARGET_LDFLAGS += -Wl,--gc-sections,--as-needed
CONFIGURE_ARGS+= \
--disable-maintainer-mode \
--disable-docs \
$(if $(CONFIG_LIBCURL_GNUTLS),--with-gnutls --without-mbedtls --without-openssl,) \
$(if $(CONFIG_LIBCURL_MBEDTLS),--without-gnutls --with-mbedtls --without-openssl,) \
$(if $(CONFIG_LIBCURL_OPENSSL),--without-gnutls --without-mbedtls --with-openssl,) \
$(if $(CONFIG_LIBCURL_WOLFSSL),--without-gnutls --with-mbedtls --without-openssl,)
define Package/uacme/conffiles
/etc/config/acme
/etc/acme
endef
define Package/uacme/install
$(INSTALL_DIR) \
$(1)/usr/sbin \
$(1)/etc/acme \
$(1)/etc/config \
$(1)/etc/init.d \
$(1)/usr/share/uacme
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/uacme $(1)/usr/sbin/uacme
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/share/uacme/uacme.sh $(1)/usr/share/uacme/
$(SED) '/^CHALLENGE_PATH=/d' $(1)/usr/share/uacme/uacme.sh
$(INSTALL_CONF) ./files/acme.config $(1)/etc/config/acme
$(INSTALL_BIN) ./files/run.sh $(1)/usr/share/uacme/run-uacme
$(INSTALL_BIN) ./files/acme.init $(1)/etc/init.d/acme
endef
$(eval $(call BuildPackage,uacme))

+ 14
- 0
net/uacme/files/acme.config View File

@ -0,0 +1,14 @@
config acme
option state_dir '/etc/acme'
option account_email 'email@example.org'
option debug 0
config cert 'example'
option enabled 0
option use_staging 1
option keylength 2048
option update_uhttpd 1
option update_nginx 1
option update_haproxy 1
option webroot "/www/.well-known/acme-challenge"
list domains example.org

+ 35
- 0
net/uacme/files/acme.init View File

@ -0,0 +1,35 @@
#!/bin/sh /etc/rc.common
USE_PROCD=1
START=50
SCRIPT=/usr/share/uacme/run-uacme
start_service()
{
procd_open_instance
procd_set_param command $SCRIPT
procd_set_param file /etc/config/acme
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
reload_service() {
rc_procd start_service "$@"
return 0
}
stop_service() {
return 0
}
boot() {
touch "/var/run/uacme_boot"
start
}
service_triggers()
{
procd_add_reload_trigger acme
}

+ 394
- 0
net/uacme/files/run.sh View File

@ -0,0 +1,394 @@
#!/bin/sh
# Wrapper for uacme to work on openwrt.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# Initial Author: Toke Høiland-Jørgensen <toke@toke.dk>
# Adapted for uacme: Lucian Cristian <lucian.cristian@gmail.com>
CHECK_CRON=$1
#check for installed packages, for now, support only one
if [ -e "/usr/lib/acme/acme.sh" ]; then
ACME=/usr/lib/acme/acme.sh
APP=acme
elif [ -e "/usr/sbin/uacme" ]; then
ACME=/usr/sbin/uacme
HPROGRAM=/usr/share/uacme/uacme.sh
APP=uacme
else
echo "Please install ACME or uACME package"
return 1
fi
export CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
export NO_TIMESTAMP=1
UHTTPD_LISTEN_HTTP=
STATE_DIR='/etc/acme'
STAGING_STATE_DIR='/etc/acme/staging'
ACCOUNT_EMAIL=
DEBUG=0
NGINX_WEBSERVER=0
UPDATE_NGINX=0
UPDATE_UHTTPD=0
UPDATE_HAPROXY=0
. /lib/functions.sh
check_cron()
{
[ -f "/etc/crontabs/root" ] && grep -q '/etc/init.d/acme' /etc/crontabs/root && return
echo "0 0 * * * /etc/init.d/acme start" >> /etc/crontabs/root
/etc/init.d/cron start
}
log()
{
logger -t $APP -s -p daemon.info "$@"
}
err()
{
logger -t $APP -s -p daemon.err "$@"
}
debug()
{
[ "$DEBUG" -eq "1" ] && logger -t $APP -s -p daemon.debug "$@"
}
get_listeners() {
local proto rq sq listen remote state program
netstat -nptl 2>/dev/null | while read proto listen program; do
case "$proto#$listen#$program" in
tcp#*:80#[0-9]*/*) echo -n "${program%% *} " ;;
esac
done
}
pre_checks()
{
main_domain="$1"
log "Running pre checks for $main_domain."
listeners="$(get_listeners)"
debug "port80 listens: $listeners"
for listener in $(get_listeners); do
pid="${listener%/*}"
cmd="${listener#*/}"
case "$cmd" in
uhttpd)
debug "Found uhttpd listening on port 80"
if [ "$APP" = "acme" ]; then
UHTTPD_LISTEN_HTTP=$(uci get uhttpd.main.listen_http)
if [ -z "$UHTTPD_LISTEN_HTTP" ]; then
err "$main_domain: Unable to find uhttpd listen config."
err "Manually disable uhttpd or set webroot to continue."
return 1
fi
uci set uhttpd.main.listen_http=''
uci commit uhttpd || return 1
if ! /etc/init.d/uhttpd reload ; then
uci set uhttpd.main.listen_http="$UHTTPD_LISTEN_HTTP"
uci commit uhttpd
return 1
fi
fi
;;
nginx*)
debug "Found nginx listening on port 80"
NGINX_WEBSERVER=1
if [ "$APP" = "acme" ]; then
local tries=0
while grep -sq "$cmd" "/proc/$pid/cmdline" && kill -0 "$pid"; do
/etc/init.d/nginx stop
if [ $tries -gt 10 ]; then
debug "Can't stop nginx. Terminating script."
return 1
fi
debug "Waiting for nginx to stop..."
tries=$((tries + 1))
sleep 1
done
fi
;;
"")
err "Nothing listening on port 80."
err "Standalone mode not supported, setup uhttpd or nginx"
return 1
;;
*)
err "$main_domain: unsupported (apache/haproxy?) daemon is listening on port 80."
err "if webroot is set on your current webserver comment line 132 (return 1) from this script."
return 1
;;
esac
done
iptables -I input_rule -p tcp --dport 80 -j ACCEPT -m comment --comment "ACME" || return 1
debug "v4 input_rule: $(iptables -nvL input_rule)"
if [ -e "/usr/sbin/ip6tables" ]; then
ip6tables -I input_rule -p tcp --dport 80 -j ACCEPT -m comment --comment "ACME" || return 1
debug "v6 input_rule: $(ip6tables -nvL input_rule)"
fi
return 0
}
post_checks()
{
log "Running post checks (cleanup)."
# The comment ensures we only touch our own rules. If no rules exist, that
# is fine, so hide any errors
iptables -D input_rule -p tcp --dport 80 -j ACCEPT -m comment --comment "ACME" 2>/dev/null
if [ -e "/usr/sbin/ip6tables" ]; then
ip6tables -D input_rule -p tcp --dport 80 -j ACCEPT -m comment --comment "ACME" 2>/dev/null
fi
if [ -e /etc/init.d/uhttpd ] && [ "$UPDATE_UHTTPD" -eq 1 ]; then
uci commit uhttpd
/etc/init.d/uhttpd reload
log "Restarting uhttpd..."
fi
if [ -e /etc/init.d/nginx ] && ( [ "$NGINX_WEBSERVER" -eq 1 ] || [ "$UPDATE_NGINX" -eq 1 ]; ); then
NGINX_WEBSERVER=0
/etc/init.d/nginx restart
log "Restarting nginx..."
fi
if [ -e /etc/init.d/haproxy ] && [ "$UPDATE_HAPROXY" -eq 1 ]; then
/etc/init.d/haproxy restart
log "Restarting haproxy..."
fi
}
err_out()
{
post_checks
exit 1
}
int_out()
{
post_checks
trap - INT
kill -INT $$
}
is_staging()
{
local main_domain="$1"
grep -q "acme-staging" "$STATE_DIR/$main_domain/${main_domain}.conf"
return $?
}
issue_cert()
{
local section="$1"
local acme_args=
local debug=
local enabled
local use_staging
local update_uhttpd
local update_nginx
local update_haproxy
local keylength
local domains
local main_domain
local failed_dir
local webroot
local dns
local ret
local staging=
local HOOK=
config_get_bool enabled "$section" enabled 0
config_get_bool use_staging "$section" use_staging
config_get_bool update_uhttpd "$section" update_uhttpd
config_get_bool update_nginx "$section" update_nginx
config_get_bool update_haproxy "$section" update_haproxy
config_get domains "$section" domains
config_get keylength "$section" keylength
config_get webroot "$section" webroot
config_get dns "$section" dns
UPDATE_NGINX=$update_nginx
UPDATE_UHTTPD=$update_uhttpd
UPDATE_HAPROXY=$update_haproxy
[ "$enabled" -eq "1" ] || return
if [ "$APP" = "uacme" ]; then
[ "$DEBUG" -eq "1" ] && debug="--verbose --verbose"
elif [ "$APP" = "acme" ]; then
[ "$DEBUG" -eq "1" ] && acme_args="$acme_args --debug"
fi
[ "$use_staging" -eq "1" ] && STATE_DIR="$STAGING_STATE_DIR" && staging="--staging"
set -- $domains
main_domain=$1
[ -n "$webroot" ] || [ -n "$dns" ] || pre_checks "$main_domain" || return 1
log "Running $APP for $main_domain"
if [ "$APP" = "uacme" ]; then
if [ ! -f "$STATE_DIR/private/key.pem" ]; then
log "Create a new ACME account with email $ACCOUNT_EMAIL use staging=$use_staging"
$ACME $debug --confdir "$STATE_DIR" $staging --yes new $ACCOUNT_EMAIL
fi
if [ -f "$STATE_DIR/$main_domain/cert.pem" ]; then
log "Found previous cert config, use staging=$use_staging. Issuing renew."
export CHALLENGE_PATH="$webroot"
$ACME $debug --confdir "$STATE_DIR" $staging --never-create issue $domains --hook=$HPROGRAM && ret=0 || ret=1
post_checks
return $ret
fi
fi
if [ "$APP" = "acme" ]; then
handle_credentials() {
local credential="$1"
eval export "$credential"
}
config_list_foreach "$section" credentials handle_credentials
if [ -e "$STATE_DIR/$main_domain" ]; then
if [ "$use_staging" -eq "0" ] && is_staging "$main_domain"; then
log "Found previous cert issued using staging server. Moving it out of the way."
mv "$STATE_DIR/$main_domain" "$STATE_DIR/$main_domain.staging"
else
log "Found previous cert config. Issuing renew."
$ACME --home "$STATE_DIR" --renew -d "$main_domain" "$acme_args" && ret=0 || ret=1
post_checks
return $ret
fi
fi
fi
acme_args="$acme_args --bits $keylength"
acme_args="$acme_args $(for d in $domains; do echo -n " $d "; done)"
if [ "$APP" = "acme" ]; then
[ -n "$ACCOUNT_EMAIL" ] && acme_args="$acme_args --accountemail $ACCOUNT_EMAIL"
[ "$use_staging" -eq "1" ] && acme_args="$acme_args --staging"
fi
if [ -n "$dns" ]; then
#TO-DO
if [ "$APP" = "acme" ]; then
log "Using dns mode"
acme_args="$acme_args --dns $dns"
else
log "Using dns mode, dns-01 is not wrapped yet"
return 1
# uacme_args="$uacme_args --dns $dns"
fi
elif [ -z "$webroot" ]; then
if [ "$APP" = "acme" ]; then
log "Using standalone mode"
acme_args="$acme_args --standalone --listen-v6"
else
log "Standalone not supported by $APP"
return 1
fi
else
if [ ! -d "$webroot" ]; then
err "$main_domain: Webroot dir '$webroot' does not exist!"
post_checks
return 1
fi
log "Using webroot dir: $webroot"
if [ "$APP" = "uacme" ]; then
export CHALLENGE_PATH="$webroot"
else
acme_args="$acme_args --webroot $webroot"
fi
fi
if [ "$APP" = "uacme" ]; then
workdir="--confdir"
HOOK="--hook=$HPROGRAM"
else
workdir="--home"
fi
if ! $ACME $debug $workdir "$STATE_DIR" $staging issue $acme_args $HOOK; then
failed_dir="$STATE_DIR/${main_domain}.failed-$(date +%s)"
err "Issuing cert for $main_domain failed. Moving state to $failed_dir"
[ -d "$STATE_DIR/$main_domain" ] && mv "$STATE_DIR/$main_domain" "$failed_dir"
[ -d "$STATE_DIR/private/$main_domain" ] && mv "$STATE_DIR/private/$main_domain" "$failed_dir"
post_checks
return 1
fi
if [ -e /etc/init.d/uhttpd ] && [ "$update_uhttpd" -eq "1" ]; then
if [ "$APP" = "uacme" ]; then
uci set uhttpd.main.key="$STATE_DIR/private/${main_domain}/key.pem"
uci set uhttpd.main.cert="$STATE_DIR/${main_domain}/cert.pem"
else
uci set uhttpd.main.key="$STATE_DIR/${main_domain}/${main_domain}.key"
uci set uhttpd.main.cert="$STATE_DIR/${main_domain}/fullchain.cer"
fi
# commit and reload is in post_checks
fi
if [ -e /etc/init.d/nginx ] && [ "$update_nginx" -eq "1" ]; then
if [ "$APP" = "uacme" ]; then
sed -i "s#ssl_certificate\ .*#ssl_certificate $STATE_DIR/${main_domain}/cert.pem;#g" /etc/nginx/nginx.conf
sed -i "s#ssl_certificate_key\ .*#ssl_certificate_key $STATE_DIR/private/${main_domain}/key.pem;#g" /etc/nginx/nginx.conf
else
sed -i "s#ssl_certificate\ .*#ssl_certificate $STATE_DIR/${main_domain}/fullchain.cer;#g" /etc/nginx/nginx.conf
sed -i "s#ssl_certificate_key\ .*#ssl_certificate_key $STATE_DIR/${main_domain}/${main_domain}.key;#g" /etc/nginx/nginx.conf
fi
# commit and reload is in post_checks
fi
if [ -e /etc/init.d/haproxy ] && [ "$update_haproxy" -eq 1 ]; then
if [ "$APP" = "uacme" ]; then
cat $STATE_DIR/${main_domain}/cert.pem $STATE_DIR/private/${main_domain}/key.pem > $STATE_DIR/${main_domain}/full_haproxy.pem
else
cat $STATE_DIR/${main_domain}/fullchain.cer $STATE_DIR/${main_domain}/${main_domain}.key > $STATE_DIR/${main_domain}/full_haproxy.pem
fi
fi
post_checks
}
load_vars()
{
local section="$1"
STATE_DIR=$(config_get "$section" state_dir)
STAGING_STATE_DIR=$STATE_DIR/staging
ACCOUNT_EMAIL=$(config_get "$section" account_email)
DEBUG=$(config_get "$section" debug)
}
check_cron
[ -n "$CHECK_CRON" ] && exit 0
[ -e "/var/run/acme_boot" ] && rm -f "/var/run/acme_boot" && exit 0
config_load acme
config_foreach load_vars acme
if [ -z "$STATE_DIR" ] || [ -z "$ACCOUNT_EMAIL" ]; then
err "state_dir and account_email must be set"
exit 1
fi
[ -d "$STATE_DIR" ] || mkdir -p "$STATE_DIR"
[ -d "$STAGING_STATE_DIR" ] || mkdir -p "$STAGING_STATE_DIR"
trap err_out HUP TERM
trap int_out INT
config_foreach issue_cert cert
exit 0

Loading…
Cancel
Save