Skip to content

Commit

Permalink
* MDContactEmail can now be specified inside a <MDomain dnsname>
Browse files Browse the repository at this point in the history
… section.
  • Loading branch information
Stefan Eissing committed Oct 19, 2021
1 parent 83addce commit 9f91ee3
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 15 deletions.
1 change: 1 addition & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
* `MDContactEmail` can now be specified inside a `<MDomain dnsname>` section.
* Added workaround for ACME servers that do not accept account retrieval
via a GET-AS-POST. This is commonly used to detect if a known account
is still usable. In case this fails, a "termsOfServiceAgreed" is set
Expand Down
6 changes: 3 additions & 3 deletions src/md_acme.c
Original file line number Diff line number Diff line change
Expand Up @@ -388,12 +388,12 @@ static apr_status_t md_acme_req_send(md_acme_req_t *req)
body = apr_pcalloc(req->p, sizeof(*body));
body->data = md_json_writep(req->req_json, req->p, MD_JSON_FMT_INDENT);
body->len = strlen(body->data);
md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, req->p,
md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, req->p,
"sending JSON body: %s", body->data);
}

if (body && md_log_is_level(req->p, MD_LOG_TRACE2)) {
md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, req->p,
if (body && md_log_is_level(req->p, MD_LOG_TRACE4)) {
md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, req->p,
"req: %s %s, body:\n%s", req->method, req->url, body->data);
}
else {
Expand Down
16 changes: 12 additions & 4 deletions src/mod_md.c
Original file line number Diff line number Diff line change
Expand Up @@ -324,11 +324,15 @@ static void merge_srv_config(md_t *md, md_srv_conf_t *base_sc, apr_pool_t *p)
md->ca_agreement = md_config_gets(md->sc, MD_CONFIG_CA_AGREEMENT);
}
contact = md_config_gets(md->sc, MD_CONFIG_CA_CONTACT);
if (contact && contact[0]) {
if (md->contacts && md->contacts->nelts > 0) {
/* set explicitly */
}
else if (contact && contact[0]) {
apr_array_clear(md->contacts);
APR_ARRAY_PUSH(md->contacts, const char *) =
md_util_schemify(p, contact, "mailto");
} else if( md->sc->s->server_admin && strcmp(DEFAULT_ADMIN, md->sc->s->server_admin)) {
}
else if( md->sc->s->server_admin && strcmp(DEFAULT_ADMIN, md->sc->s->server_admin)) {
apr_array_clear(md->contacts);
APR_ARRAY_PUSH(md->contacts, const char *) =
md_util_schemify(p, md->sc->s->server_admin, "mailto");
Expand Down Expand Up @@ -596,14 +600,18 @@ static apr_status_t link_md_to_servers(md_mod_conf_t *mc, md_t *md, server_rec *
s->server_hostname, s->port, md->name, sc->name,
domain, (int)sc->assigned->nelts);

if (sc->ca_contact && sc->ca_contact[0]) {
if (md->contacts && md->contacts->nelts > 0) {
/* set explicitly */
}
else if (sc->ca_contact && sc->ca_contact[0]) {
uri = md_util_schemify(p, sc->ca_contact, "mailto");
if (md_array_str_index(md->contacts, uri, 0, 0) < 0) {
APR_ARRAY_PUSH(md->contacts, const char *) = uri;
ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10044)
"%s: added contact %s", md->name, uri);
}
} else if (s->server_admin && strcmp(DEFAULT_ADMIN, s->server_admin)) {
}
else if (s->server_admin && strcmp(DEFAULT_ADMIN, s->server_admin)) {
uri = md_util_schemify(p, s->server_admin, "mailto");
if (md_array_str_index(md->contacts, uri, 0, 0) < 0) {
APR_ARRAY_PUSH(md->contacts, const char *) = uri;
Expand Down
5 changes: 5 additions & 0 deletions src/mod_md_config.c
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,11 @@ static void srv_conf_props_apply(md_t *md, const md_srv_conf_t *from, apr_pool_t
if (from->ca_url) md->ca_url = from->ca_url;
if (from->ca_proto) md->ca_proto = from->ca_proto;
if (from->ca_agreement) md->ca_agreement = from->ca_agreement;
if (from->ca_contact) {
apr_array_clear(md->contacts);
APR_ARRAY_PUSH(md->contacts, const char *) =
md_util_schemify(p, from->ca_contact, "mailto");
}
if (from->ca_challenges) md->ca_challenges = apr_array_copy(p, from->ca_challenges);
if (from->ca_eab_kid) md->ca_eab_kid = from->ca_eab_kid;
if (from->ca_eab_hmac) md->ca_eab_hmac = from->ca_eab_hmac;
Expand Down
4 changes: 2 additions & 2 deletions test/md_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def __init__(self, env: MDTestEnv, text=None, std_ports=True,
self._lines = []

if admin is None:
admin = "admin@not-forbidden.org"
admin = f"admin@{env.http_tld}"
if len(admin.strip()):
self.add_admin(admin)

Expand Down Expand Up @@ -70,7 +70,7 @@ def add_private_key(self, key_type, key_params):
self.add("MDPrivateKeys %s %s\n" % (key_type, " ".join(map(lambda p: str(p), key_params))))

def add_admin(self, email):
self.add("ServerAdmin mailto:%s\n" % email)
self.add(f"ServerAdmin mailto:{email}")

def add_md(self, domains):
dlist = " ".join(domains) # without quotes
Expand Down
15 changes: 9 additions & 6 deletions test/md_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ def __init__(self, pytestconfig=None):
self._httpd_base_conf = [
f"LoadModule mpm_{self.mpm_type}_module \"{self.libexec_dir}/mod_mpm_{self.mpm_type}.so\"",
f"LoadModule {self._ssl_type}_module \"{self.prefix}/modules/mod_{self._ssl_type}.so\"",
f"LogLevel {self._ssl_type}:debug",
f"LogLevel {self._ssl_type}:info",
f"SSLSessionCache \"shmcb:ssl_gcache_data(32000)\"",
"",
]
Expand Down Expand Up @@ -512,12 +512,12 @@ def a2md(self, args, raw=False) -> ExecResult:
log.debug("running: {0} {1}".format(preargs, args))
return self.run(preargs + args)

def curl_complete_args(self, urls, timeout=None, options=None, insecure=False):
def curl_complete_args(self, urls, timeout=None, options=None,
insecure=False, force_resolve=True):
if not isinstance(urls, list):
urls = [urls]
u = urlparse(urls[0])
assert u.hostname, f"hostname not in url: {urls[0]}"
assert u.port, f"port not in url: {urls[0]}"
headerfile = f"{self.gen_dir}/curl.headers"
if os.path.isfile(headerfile):
os.remove(headerfile)
Expand All @@ -536,8 +536,10 @@ def curl_complete_args(self, urls, timeout=None, options=None, insecure=False):
else:
args.extend(["--cacert", self.acme_ca_pemfile])

if u.hostname != 'localhost' and u.hostname != self._httpd_addr \
if force_resolve and u.hostname != 'localhost' \
and u.hostname != self._httpd_addr \
and not re.match(r'^(\d+|\[|:).*', u.hostname):
assert u.port, f"port not in url: {urls[0]}"
args.extend(["--resolve", f"{u.hostname}:{u.port}:{self._httpd_addr}"])
if timeout is not None and int(timeout) > 0:
args.extend(["--connect-timeout", str(int(timeout))])
Expand All @@ -547,9 +549,10 @@ def curl_complete_args(self, urls, timeout=None, options=None, insecure=False):
return args, headerfile

def curl_raw(self, urls, timeout=10, options=None, insecure=False,
debug_log=True):
debug_log=True, force_resolve=True):
args, headerfile = self.curl_complete_args(
urls=urls, timeout=timeout, options=options, insecure=insecure)
urls=urls, timeout=timeout, options=options, insecure=insecure,
force_resolve=force_resolve)
r = self.run(args, debug_log=debug_log)
if r.exit_code == 0:
lines = open(headerfile).readlines()
Expand Down
202 changes: 202 additions & 0 deletions test/test_752_zerossl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import os
import time

import pytest

from md_conf import HttpdConf

# set the environment variables
# ZEROSSL_TLD="<your registered dns name>"
# these tests to become active
#

DEMO_ACME = "https://acme.zerossl.com/v2/DV90"
DEMO_EAB_URL = "http://api.zerossl.com/acme/eab-credentials-email"
DEMO_TLD = None


def missing_tld():
global DEMO_TLD
if 'ZEROSSL_TLD' in os.environ:
DEMO_TLD = os.environ['ZEROSSL_TLD']
return DEMO_TLD is None


def get_new_eab(env):
r = env.curl_raw(DEMO_EAB_URL, options=[
"-d", f"email=admin@zerossl.{DEMO_TLD}"
], force_resolve=False)
assert r.exit_code == 0
assert r.json
assert r.json['success'] is True
assert r.json['eab_kid']
assert r.json['eab_hmac_key']
return {'kid': r.json['eab_kid'], 'hmac': r.json['eab_hmac_key']}


@pytest.mark.skipif(condition=missing_tld(), reason="env var ZEROSSL_TLD not set")
class TestZeroSSL:

@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env, acme):
acme.start(config='eab')
env.check_acme()
env.clear_store()
HttpdConf(env).install()
assert env.apache_restart() == 0

@pytest.fixture(autouse=True, scope='function')
def _method_scope(self, env, request):
self.test_domain = env.get_request_domain(request)

def test_752_001(self, env):
# valid config, expect cert with correct chain
domain = f"test1.{DEMO_TLD}"
domains = [domain]
eab = get_new_eab(env)
conf = HttpdConf(env)
conf.start_md(domains)
conf.add(f"""
MDCertificateAuthority {DEMO_ACME}
MDCertificateAgreement accepted
MDContactEmail admin@zerossl.{DEMO_TLD}
MDCACertificateFile none
MDExternalAccountBinding {eab['kid']} {eab['hmac']}
""")
conf.end_md()
conf.add_vhost(domains=domains)
conf.install()
assert env.apache_restart() == 0
assert env.await_completion(domains)
r = env.curl_get(f"https://{domain}:{env.https_port}", options=[
"--cacert", f"{env.test_dir}/data/sectigo-demo-root.pem"
])
assert r.response['status'] == 200

def test_752_002(self, env):
# without EAB set
domain = f"test1.{DEMO_TLD}"
domains = [domain]
conf = HttpdConf(env)
conf.start_md(domains)
conf.add(f"""
MDCertificateAuthority {DEMO_ACME}
MDCertificateAgreement accepted
MDContactEmail admin@zerossl.{DEMO_TLD}
MDCACertificateFile none
""")
conf.end_md()
conf.add_vhost(domains=domains)
conf.install()
assert env.apache_restart() == 0
assert env.await_error(domain)
md = env.get_md_status(domain)
assert md['renewal']['errors'] > 0
assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:externalAccountRequired'

def test_752_003(self, env):
# with wrong EAB set
domain = f"test1.{DEMO_TLD}"
domains = [domain]
conf = HttpdConf(env)
conf.start_md(domains)
conf.add(f"""
MDCertificateAuthority {DEMO_ACME}
MDCertificateAgreement accepted
MDContactEmail admin@zerossl.{DEMO_TLD}
MDCACertificateFile none
""")
conf.add(f"MDExternalAccountBinding YmxhYmxhYmxhCg YmxhYmxhYmxhCg")
conf.end_md()
conf.add_vhost(domains=domains)
conf.install()
assert env.apache_restart() == 0
assert env.await_error(domain)
md = env.get_md_status(domain)
assert md['renewal']['errors'] > 0
assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:malformed'

def test_752_004(self, env):
# valid config, get cert, add dns name, renew cert
domain = f"test1.{DEMO_TLD}"
domain2 = f"test2.{DEMO_TLD}"
domains = [domain]
eab = get_new_eab(env)
conf = HttpdConf(env)
conf.start_md(domains)
conf.add(f"""
MDCertificateAuthority {DEMO_ACME}
MDCertificateAgreement accepted
MDContactEmail admin@zerossl.{DEMO_TLD}
MDCACertificateFile none
MDExternalAccountBinding {eab['kid']} {eab['hmac']}
""")
conf.end_md()
conf.add_vhost(domains=domains)
conf.install()
assert env.apache_restart() == 0
assert env.await_completion(domains)
r = env.curl_get(f"https://{domain}:{env.https_port}", options=[
"--cacert", f"{env.test_dir}/data/sectigo-demo-root.pem"
])
assert r.response['status'] == 200
r = env.curl_get(f"https://{domain2}:{env.https_port}", options=[
"--cacert", f"{env.test_dir}/data/sectigo-demo-root.pem"
])
assert r.exit_code != 0
md1 = env.get_md_status(domain)
acct1 = md1['ca']['account']
# add the domain2 to the dns names
domains = [domain, domain2]
conf = HttpdConf(env)
conf.start_md(domains)
conf.add(f"""
MDCertificateAuthority {DEMO_ACME}
MDCertificateAgreement accepted
MDContactEmail admin@zerossl.{DEMO_TLD}
MDCACertificateFile none
MDExternalAccountBinding {eab['kid']} {eab['hmac']}
""")
conf.end_md()
conf.add_vhost(domains=domains)
conf.install()
assert env.apache_restart() == 0
assert env.await_completion(domains)
r = env.curl_get(f"https://{domain2}:{env.https_port}", options=[
"--cacert", f"{env.test_dir}/data/sectigo-demo-root.pem"
])
assert r.response['status'] == 200
md2 = env.get_md_status(domain)
acct2 = md2['ca']['account']
assert acct2 == acct1, f"ACME account was not reused: {acct1} became {acct2}"

def test_752_020(self, env):
# valid config, get cert, check OCSP status
domain = f"test1.{DEMO_TLD}"
domains = [domain]
eab = get_new_eab(env)
conf = HttpdConf(env)
conf.add("MDStapling on")
conf.start_md(domains)
conf.add(f"""
MDCertificateAuthority {DEMO_ACME}
MDCertificateAgreement accepted
MDContactEmail admin@zerossl.{DEMO_TLD}
MDCACertificateFile none
MDExternalAccountBinding {eab['kid']} {eab['hmac']}
""")
conf.end_md()
conf.add_vhost(domains=domains)
conf.install()
assert env.apache_restart() == 0
assert env.await_completion(domains)
r = env.curl_get(f"https://{domain}:{env.https_port}", options=[
"--cacert", f"{env.test_dir}/data/sectigo-demo-root.pem"
])
assert r.response['status'] == 200
time.sleep(1)
for domain in domains:
stat = env.await_ocsp_status(domain,
ca_file=f"{env.test_dir}/data/sectigo-demo-root.pem")
assert stat['ocsp'] == "successful (0x0)"
assert stat['verify'] == "0 (ok)"

0 comments on commit 9f91ee3

Please sign in to comment.