]> granicus.if.org Git - apache/commitdiff
Set of changes to bring mod_md into the 2.4.x branch.
authorStefan Eissing <icing@apache.org>
Mon, 27 Nov 2017 10:44:56 +0000 (10:44 +0000)
committerStefan Eissing <icing@apache.org>
Mon, 27 Nov 2017 10:44:56 +0000 (10:44 +0000)
git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/branches/2.4.x-mod_md@1816423 13f79535-47bb-0310-9956-ffa450edef68

49 files changed:
docs/manual/mod/mod_md.xml [new file with mode: 0644]
docs/manual/mod/mod_md.xml.meta [new file with mode: 0644]
docs/manual/sections.xml
modules/md/Makefile [new file with mode: 0644]
modules/md/Makefile.in [new file with mode: 0644]
modules/md/config2.m4 [new file with mode: 0644]
modules/md/md.h [new file with mode: 0644]
modules/md/md_acme.c [new file with mode: 0644]
modules/md/md_acme.h [new file with mode: 0644]
modules/md/md_acme_acct.c [new file with mode: 0644]
modules/md/md_acme_acct.h [new file with mode: 0644]
modules/md/md_acme_authz.c [new file with mode: 0644]
modules/md/md_acme_authz.h [new file with mode: 0644]
modules/md/md_acme_drive.c [new file with mode: 0644]
modules/md/md_core.c [new file with mode: 0644]
modules/md/md_crypt.c [new file with mode: 0644]
modules/md/md_crypt.h [new file with mode: 0644]
modules/md/md_curl.c [new file with mode: 0644]
modules/md/md_curl.h [new file with mode: 0644]
modules/md/md_http.c [new file with mode: 0644]
modules/md/md_http.h [new file with mode: 0644]
modules/md/md_json.c [new file with mode: 0644]
modules/md/md_json.h [new file with mode: 0644]
modules/md/md_jws.c [new file with mode: 0644]
modules/md/md_jws.h [new file with mode: 0644]
modules/md/md_log.c [new file with mode: 0644]
modules/md/md_log.h [new file with mode: 0644]
modules/md/md_reg.c [new file with mode: 0644]
modules/md/md_reg.h [new file with mode: 0644]
modules/md/md_store.c [new file with mode: 0644]
modules/md/md_store.h [new file with mode: 0644]
modules/md/md_store_fs.c [new file with mode: 0644]
modules/md/md_store_fs.h [new file with mode: 0644]
modules/md/md_util.c [new file with mode: 0644]
modules/md/md_util.h [new file with mode: 0644]
modules/md/md_version.h [new file with mode: 0644]
modules/md/mod_md.c [new file with mode: 0644]
modules/md/mod_md.dsp [new file with mode: 0644]
modules/md/mod_md.h [new file with mode: 0644]
modules/md/mod_md_config.c [new file with mode: 0644]
modules/md/mod_md_config.h [new file with mode: 0644]
modules/md/mod_md_os.c [new file with mode: 0644]
modules/md/mod_md_os.h [new file with mode: 0644]
modules/md/mod_md_private.h [new file with mode: 0644]
modules/ssl/ssl_engine_init.c
modules/ssl/ssl_engine_kernel.c
modules/ssl/ssl_private.h
modules/ssl/ssl_util_ssl.c
modules/ssl/ssl_util_ssl.h

diff --git a/docs/manual/mod/mod_md.xml b/docs/manual/mod/mod_md.xml
new file mode 100644 (file)
index 0000000..e1095e2
--- /dev/null
@@ -0,0 +1,584 @@
+<?xml version="1.0"?>
+<!DOCTYPE modulesynopsis SYSTEM "../style/modulesynopsis.dtd">
+<?xml-stylesheet type="text/xsl" href="../style/manual.en.xsl"?>
+<!-- $LastChangedRevision$ -->
+
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License.  You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<modulesynopsis metafile="mod_md.xml.meta">
+    
+    <name>mod_md</name>
+    <description>Managing domains across virtual hosts, certificate provisioning 
+        via the ACME protocol
+    </description>
+    <status>Extension</status>
+    <sourcefile>mod_md.c</sourcefile>
+    <identifier>md_module</identifier>
+    <compatibility>Available in version 2.5.0 and later</compatibility>
+    <summary>
+        <p>
+        This module manages common properties of domains for one or more virtual hosts. 
+        Specifically it can use the ACME protocol
+        (<a href="https://datatracker.ietf.org/doc/draft-ietf-acme-acme/">RFC Draft</a>) 
+        to automate certificate provisioning. These will be configured for managed domains and
+        their virtual hosts automatically. This includes renewal of certificates before they
+        expire. The most famous Certificate Authority currently implementing the ACME protocol
+        is <a href="https://letsencrypt.org/">Let's Encrypt</a>.</p>
+        
+        <note type="warning"><title>Warning</title>
+            <p>This module is experimental. Its behaviors, directives, and 
+                defaults are subject to more change from release to 
+                release relative to other standard modules. Users are encouraged to 
+                consult the "CHANGES" file for potential updates.</p>
+        </note>
+
+        <p>Simple configuration example:</p>
+        
+        <note><title>TLS in a VirtualHost context</title>
+        <highlight language="config">
+ManagedDomain example.org
+
+&lt;VirtualHost *:443&gt;
+    ServerName example.org
+    DocumentRoot htdocs/a
+
+    SSLEngine on
+    # no certificates specification needed!
+&lt;/VirtualHost&gt;
+        </highlight>
+        <p>
+            This setup will, on server start, contact
+            <a href="https://letsencrypt.org/">Let's Encrypt</a>
+            to request a certificate for the domain. If Let's Encrypt can verify the ownership
+            of the domain, the module will retrieve the certificate and its chain, store it
+            in the local file system (see <directive module="mod_md">MDStoreDir</directive>)
+            and provide it, on next restart, to mod_ssl.
+        </p><p>
+            This happens while the server is already running. All other hosts will continue
+            to work as before. While a certificate is not available, requests for the managed
+            domain will be answered with a '503 Service Unavailable'. 
+        </p>
+        </note>
+
+    </summary>
+    
+    <directivesynopsis>
+        <name>ManagedDomain</name>
+        <description>Define list of domain names that belong to one group.</description>
+        <syntax>ManagedDomain <var>dns-name</var> [ <var>other-dns-name</var>... ]</syntax>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        
+        <usage>
+            <p>
+                All the names in the list are managed as one Managed Domain (MD). 
+                mod_md will request one single certificate that is valid for all these names. This
+                directive uses the global settings (see other MD directives below). If you
+                need specific settings for one MD, use
+                the <directive module="mod_md" type="section">ManagedDomain</directive>.
+            </p><p>
+                There are 2 additional settings that are necessary for a Managed Domain:
+                <directive module="core">ServerAdmin</directive>
+                and <directive module="mod_md">MDCertificateAgreement</directive>.
+                The mail address of <directive module="core">ServerAdmin</directive>
+                is used to register at the CA (Let's Encrypt by default).
+                The CA may use it to notify you about
+                changes in its service or status of your certificates.
+            </p><p>
+                The second setting, <directive module="mod_md">MDCertificateAgreement</directive>, 
+                is the URL of the Terms of Service of the CA. When you configure the URL, 
+                you confirm that you have read and agree to the terms described in the linked 
+                document. Before you do that, the CA will  not hand out certificates to you.
+            </p>
+            <example><title>Example</title>
+                <highlight language="config">
+ServerAdmin mailto:admin@example.org
+MDCertificateAgreement https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf
+ManagedDomain example.org www.example.org
+
+&lt;VirtualHost *:443&gt;
+    ServerName example.org
+    DocumentRoot htdocs/root
+
+    SSLEngine on
+&lt;/VirtualHost&gt;
+
+&lt;VirtualHost *:443&gt;
+    ServerName www.example.org
+    DocumentRoot htdocs/www
+
+    SSLEngine on
+&lt;/VirtualHost&gt;
+                </highlight>
+            </example>
+            <p>
+                There are two special names that you may use in this directive: 'manual'
+                and 'auto'. This determines if a Managed Domain shall have exactly the 
+                name list as is configured ('manual') or offer more convenience. With 'auto'
+                all names of a virtual host are added to a MD.
+            </p>
+            <example><title>Example</title>
+                <highlight language="config">
+ManagedDomain example.org
+
+&lt;VirtualHost *:443&gt;
+    ServerName example.org
+    ServerAlias www.example.org
+    DocumentRoot htdocs/root
+
+    SSLEngine on
+&lt;/VirtualHost&gt;
+                </highlight>
+            </example>
+            <p>
+                In this example, the domain 'www.example.org' is automatically added to
+                the MD 'example.org'. And when you add more ServerAlias names to this
+                virtual host, they will be added as well.
+            </p><p>
+                If this is too much automagic for you, define 'manual' mode. 
+                <module>mod_md</module> will then complain if the names do not match.
+            </p>
+        </usage>
+    </directivesynopsis>
+
+    <directivesynopsis type="section" idtype="section">
+        <name>ManagedDomain</name>
+        <description>Container for directives applied to the same managed domains.</description>
+        <syntax>&lt;ManagedDomain <var>dns-name</var> [ <var>other-dns-name</var>... ]&gt;...&lt;/ManagedDomain&gt;</syntax>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        
+        <usage>
+            <p>
+                This directive allows you to define a Managed Domain (MD) with specific
+                settings, different from the global MD* ones. For example, you can have
+                such an MD use another CA then Let's Encrypt, have its unique renewal duration
+                etc.
+            </p>
+            <example><title>Example</title>
+                <highlight language="config">
+&lt;ManagedDomain sandbox.example.org&gt;
+    MDDriveMode manual
+    MDCertificateAuthority   https://someotherca.com/ACME
+    MDCertificateAgreement   https://someotherca.com/terms/v_1.02.pdf
+&lt;/ManagedDomain&gt;
+                </highlight>
+            </example>
+        </usage>
+    </directivesynopsis>
+
+    <directivesynopsis>
+        <name>MDCertificateAgreement</name>
+        <description>The URL of the Terms-of-Service document, that the CA server requires you to accept.</description>
+        <syntax>MDCertificateAgreement url-of-terms-of-service</syntax>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <usage>
+            <p>When you use <module>mod_md</module> to obtain a certificate, you become a customer of the CA (e.g. Let's Encrypt). That means you need to read and agree to their Terms of Service, 
+            so that you understand what they offer and what they might exclude or require from you. 
+            <module>mod_md</module> cannot, by itself, agree to such a thing. 
+            </p>
+            <p>In case of Let's Encrypt, their current <a href="https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf">Terms of Service are here</a>. 
+            Those terms might (and probably will) change over time. So, the certificate renewal might require you to update this agreement URL.</p>
+            <example><title>Example</title>
+                <highlight language="config">
+MDCertificateAgreement https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf
+ManagedDomain example.org www.example.org mail.example.org
+                </highlight>
+            </example>
+        </usage>
+    </directivesynopsis>
+
+    <directivesynopsis>
+        <name>MDCertificateAuthority</name>
+        <description>The URL of the ACME Certificate Authority service.</description>
+        <syntax>MDCertificateAuthority url</syntax>
+        <default>MDCertificateAuthority https://acme-v01.api.letsencrypt.org/directory</default>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <usage>
+            <p>
+                The URL where the CA offers its service.
+            </p><p>
+                Let's Encrypt offers, right now, two such URLs. One for the real certificates and
+                one for testing (their staging area, at https://acme-staging.api.letsencrypt.org/directory).
+                In order to have <module>mod_md</module> use this testing service, configure your
+                server like this: 
+            </p>
+            <example><title>LE Staging Setup</title>
+                <highlight language="config">
+MDCertificateAuthority https://acme-staging.api.letsencrypt.org/directory
+MDCertificateAgreement https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf
+                </highlight>
+            </example>
+        </usage>
+    </directivesynopsis>
+
+    <directivesynopsis>
+        <name>MDCertificateProtocol</name>
+        <description>The protocol to use with the Certificate Authority.</description>
+        <syntax>MDCertificateProtocol protocol</syntax>
+        <default>MDCertificateProtocol ACME</default>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <usage>
+            <p>Specifies the protocol to use. Currently, only <code>ACME</code> is supported.</p>
+        </usage>
+    </directivesynopsis>
+
+    <directivesynopsis>
+        <name>MDDriveMode</name>
+        <description>Control when it is allowed to obtain/renew certificates.</description>
+        <syntax>MDDriveMode always|auto|manual</syntax>
+        <default>MDDriveMode auto</default>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <usage>
+            <p>In 'auto' mode, <module>mod_md</module> will <em>drive</em> a Managed Domain's
+            properties (e.g. certificate management) whenever necessary. When a MD is not used
+            in any virtual host, the module will do nothing. When a certificate is missing, it
+            will try to get one. When a certificate expires soon (see 
+            <directive module="mod_md">MDRenewWindow</directive>), it will
+            renew it.
+            </p><p>
+            In 'manual' mode, it is your duty to do all this. The module will provide the existing
+            certificate to mod_ssl, if available. But it will not contact the CA for signup/renewal.
+            This can be useful in clustered setups where you want just one node to perform
+            the driving.
+            </p><p>
+            The third mode 'always' is like 'auto' only that <module>mod_md</module> will not
+            check if the MD is actually used somewhere. 
+            </p>
+        </usage>
+    </directivesynopsis>
+
+    <directivesynopsis>
+        <name>MDHttpProxy</name>
+        <description>Define a proxy for outgoing connections.</description>
+        <syntax>MDHttpProxy url</syntax>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <usage>
+            <p>Use a http proxy to connect to the MDCertificateAuthority. Define this
+            if your webserver can only reach the internet with a forward proxy.
+            </p>
+        </usage>
+    </directivesynopsis>
+
+    <directivesynopsis>
+        <name>MDMember</name>
+        <description>Additional hostname for the managed domain.</description>
+        <syntax>MDMember hostname</syntax>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <usage>
+            <p>
+            Instead of listing all dns names on the same line, you may use
+            <directive module="mod_md">MDMember</directive> to add such names
+            to a managed domain.
+            </p>
+            <example><title>Example</title>
+                <highlight language="config">
+&lt;ManagedDomain example.org&gt;
+    MDMember www.example.org
+    MDMember mail.example.org
+&lt;/ManagedDomain example.org&gt;
+                </highlight>
+            </example>
+            <p>
+               If you use it in the global context, outside a specific MD, you can only
+               specify one value, 'auto' or 'manual' as the default for all other MDs. See
+               <directive module="mod_md" type="section">ManagedDomain</directive> for a
+               description of these special values.
+            </p>
+        </usage>
+    </directivesynopsis>
+
+    <directivesynopsis>
+        <name>MDMembers</name>
+        <description>Control if the alias domain names are automatically added.</description>
+        <syntax>MDMembers auto|manual</syntax>
+        <default>MDDriveMode auto</default>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <usage>
+            <p>Defines if the <directive module="core">ServerName</directive> and
+               <directive module="core">ServerAlias</directive> values of a VirtualHost
+               are automatically added to the members of a Managed Domain or not.
+            </p>
+        </usage>
+    </directivesynopsis>
+
+    <directivesynopsis>
+        <name>MDMustStaple</name>
+        <description>Control if new certificates carry the OCSP Must Staple flag.</description>
+        <syntax>MDMustStaple on|off</syntax>
+        <default>MDMustStaple off</default>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <usage>
+            <p>Defines if newly requested certificate should have the OCSP Must Staple flag 
+            set or not. If a certificate has this flag, the server is required to send a 
+            OCSP stapling response to every client. This only works if you configure 
+            mod_ssl to generate this (see <directive module="mod_ssl" >SSLUseStapling</directive>
+            and friends).
+            </p>
+        </usage>
+    </directivesynopsis>
+
+    <directivesynopsis>
+        <name>MDNotifyCmd</name>
+        <description>Run a program when Managed Domain are ready.</description>
+        <syntax>MDNotifyCmd  path</syntax>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <usage>
+            <p>The configured executable is run when Managed Domains have signed up or
+            renewed their certificates. It is given the names of the processed MDs as
+            arguments. It should return status code 0 to indicate that it has 
+            run successfully.
+            </p>
+        </usage>
+    </directivesynopsis>
+
+    <directivesynopsis>
+        <name>MDPortMap</name>
+        <description>Map external to internal ports for domain ownership verification.</description>
+        <syntax>MDPortMap map1 [ map2 ]</syntax>
+        <default>MDPortMap 80:80 443:443</default>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <usage>
+            <p>
+                The ACME protocol provides two method to verify domain ownership: one that uses
+                port 80 and one for port 443. If your server is not reachable by at least one
+                of the two, ACME will not work for you.
+            </p><p>
+                <module>mod_md</module> will look at your server configuration and try to figure
+                out which of those are available. Then it can select the proper ACME challenge
+                to create a certificate for your site.
+            </p><p>
+                However if you have some fancy port forwarding in place, your server may be
+                reachable from the Internet on port 443, but the local port that httpd uses is
+                another one. Your server might only listen on ports 5001 and 5002, but be reached
+                on ports 443 and 80. How should <module>mod_md</module> figure that one out?
+            </p><p>
+                With MDPortMap you can tell it which 'Internet port' corresponds to which local
+                port.
+            </p>
+            <example><title>Example</title>
+                <highlight language="config">
+MDPortMap 80:- 443:5002
+                </highlight>
+            </example>
+            <p>
+                This example says that the server is not reachable on port 80 from the outside, but
+                local port 5002 is the one responding to https: requests.
+            </p>
+        </usage>
+    </directivesynopsis>
+
+    <directivesynopsis>
+        <name>MDPrivateKeys</name>
+        <description>Set type and size of the private keys generated.</description>
+        <syntax>MDPrivateKeys type [ params... ]</syntax>
+        <default>MDPrivateKeys RSA 2048</default>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <usage>
+            <p>
+                Defines what kind of private keys are generated for a managed domain and with
+                what parameters. The only supported type right now is 'RSA' and the only parameter
+                it takes is the number of bits used for the key.
+            </p><p>
+                The current (2017) recommendation is at least 2048 bits and a smaller number is
+                not accepted here. Higher numbers offer longer security, but are computationally more 
+                expensive, e.g. increase the load on your server. That might or might not be an
+                issue for you.
+            </p><p>
+                Other key types will be defined in the future.
+            </p>
+            <example><title>Example</title>
+                <highlight language="config">
+MDPrivateKeys RSA 3072
+                </highlight>
+            </example>
+            <p>
+                Please note that this setting only has an effect on new keys. Any existing
+                private key you have remains unaffected. Also, this only affects private keys
+                generated for certificates. ACME account keys are unaffected by this.
+            </p>
+        </usage>
+    </directivesynopsis>
+
+    <directivesynopsis>
+        <name>MDRenewWindow</name>
+        <description>Control when a certificate will be renewed.</description>
+        <syntax>MDRenewWindow duration</syntax>
+        <default>MDRenewWindow 33%</default>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <usage>
+            <p>
+            If the validity of the certificate falls below duration, mod_md will get a 
+            new signed certificate.
+            </p><p>
+            Normally, certificates are valid for around 90 days and mod_md will renew 
+            them the earliest 33% of their complete lifetime before they expire (so for 
+            90 days validity, 30 days before it expires). If you think this is not what 
+            you need, you can specify either the exact time, as in:
+            </p>
+            <example><title>Example</title>
+                <highlight language="config">
+# 21 days before expiry
+MDRenewWindow 21d 
+# 30 seconds (might be close)
+MDRenewWindow 30s
+# 10% of the cert lifetime
+MDRenewWindow 10%
+                </highlight>
+            </example>
+            <p>When in auto drive mode, the module will check every 12 hours at least 
+            what the status of the managed domains is and if it needs to do something. 
+            On errors, for example when the CA is unreachable, it will initially retry 
+            after some seconds. Should that continue to fail, it will back off to a 
+            maximum interval of hourly checks.
+            </p>
+        </usage>
+    </directivesynopsis>
+
+    <directivesynopsis>
+        <name>MDRequireHttps</name>
+        <description>Redirects http: traffic to https: for Managed Domains.</description>
+        <syntax>MDRequireHttps off|temporary|permanent</syntax>
+        <default>MDRequireHttps off</default>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <usage>
+            <p>This is a convenience directive to ease http: to https: migration of 
+            your Managed Domains. With:
+            </p>
+            <example><title>Example</title>
+                <highlight language="config">
+MDRequireHttps temporary                
+                </highlight>
+            </example>
+            <p>you announce that you want all traffic via http: URLs to be redirected 
+            to the https: ones, for now. This is safe and you can remove this again at
+            any time.
+            </p><p>
+                <strong>The following has consequences: </strong>if you want client to <strong>no longer</strong> use the
+             http: URLs, configure:
+            </p>
+            <example><title>Permanent (for at least half a year!)</title>
+                <highlight language="config">
+MDRequireHttps permanent                
+                </highlight>
+            </example>
+            <p>This does two things:
+            </p>
+            <ol>
+                <li>All request to the <code>http:</code> resources are redirected to the
+                    same url with the <code>https:</code> scheme using the <code>301</code>
+                status code. This tells clients that this is intended to be forever and
+                the should update any links they have accordingly.
+                </li>
+                <li>All answers to <code>https:</code> requests will carry the header
+                    <code>Strict-Transport-Security</code> with a life time of half a year.
+                    This tells the browser that it <strong>never</strong> (for half a year) shall use <code>http:</code>
+                    when talking to this domain name. Browsers will, after having seen this, refuse
+                    to contact your unencrypted site. This prevents malicious middleware to
+                    downgrade connections and listen/manipulate the traffic. Which is good. But
+                    you cannot simply take it back again.
+                </li>
+            </ol>
+            <p>You can achieve the same with mod_alias and some Redirect configuration,
+            basically. If you do it yourself, please make sure to exclude the paths 
+            /.well-known/* from your redirection, otherwise mod_md might have trouble 
+            signing on new certificates.
+            </p>
+            <p>If you set this globally, it applies to all managed domains. If you want 
+            it for a specific domain only, use:
+            </p>
+            <example><title>Example</title>
+                <highlight language="config">
+&lt;ManagedDomain xxx.yyy&gt;
+  MDRequireHttps temporary
+&lt;/ManagedDomain&gt;
+                </highlight>
+            </example>
+        </usage>
+    </directivesynopsis>
+
+    <directivesynopsis>
+        <name>MDStoreDir</name>
+        <description>Path on the local file system to store the Managed Domains data.</description>
+        <syntax>MDStoreDir path</syntax>
+        <default>MDStoreDir md</default>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <usage>
+            <p>
+                Defines where on the local file system the Managed Domain data is stored. This is
+                an absolute path or interpreted relative to the server root. The default will create
+                a directory 'md' in your server root.
+            </p><p>
+                If you move this and have already data, be sure to move/copy the data first to
+                the new location, reconfigure and then restart the server. If you reconfigure
+                and restart first, the server will try to get new certificates that it thinks
+                are missing.
+            </p>
+        </usage>
+    </directivesynopsis>
+
+    <directivesynopsis>
+        <name>MDCAChallenges</name>
+        <description>Type of ACME challenge used to prove domain ownership.</description>
+        <syntax>MDCAChallenges name [ name ... ]</syntax>
+        <default>MDCAChallenges tls-sni-01 http-01</default>
+        <contextlist>
+            <context>server config</context>
+        </contextlist>
+        <usage>
+            <p>
+                This tells <module>mod_md</module> which challenge types it shall use in
+                which order when proving domain ownership. The names are protocol specific. The
+                current ACME protocol version that Let's Encrypt speaks defines two challenge
+                types that are supported by <module>mod_md</module>. By default, it will try
+                the one on port 443 when available.
+            </p>
+        </usage>
+    </directivesynopsis>
+
+</modulesynopsis>
diff --git a/docs/manual/mod/mod_md.xml.meta b/docs/manual/mod/mod_md.xml.meta
new file mode 100644 (file)
index 0000000..29abbf4
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!-- GENERATED FROM XML: DO NOT EDIT -->
+
+<metafile reference="mod_md.xml">
+  <basename>mod_md</basename>
+  <path>/mod/</path>
+  <relpath>..</relpath>
+
+  <variants>
+    <variant>en</variant>
+  </variants>
+</metafile>
index 08807e46a403ddc1668c15ae35ed99dd2741547b..e466610d163b3b090c8962bb7a3675347cc8267d 100644 (file)
@@ -51,6 +51,7 @@ to change the scope of other configuration directives.</p>
 <directive type="section" module="mod_version">IfVersion</directive>
 <directive type="section" module="core">Location</directive>
 <directive type="section" module="core">LocationMatch</directive>
+<directive type="section" module="md">MangedDomain</directive>
 <directive type="section" module="mod_proxy">Proxy</directive>
 <directive type="section" module="mod_proxy">ProxyMatch</directive>
 <directive type="section" module="core">VirtualHost</directive>
diff --git a/modules/md/Makefile b/modules/md/Makefile
new file mode 100644 (file)
index 0000000..4a83a8a
--- /dev/null
@@ -0,0 +1,25 @@
+top_srcdir   = /Users/sei/projects/httpd/2.4.x
+top_builddir = /Users/sei/projects/httpd/2.4.x
+srcdir       = /Users/sei/projects/httpd/2.4.x/modules/md
+builddir     = /Users/sei/projects/httpd/2.4.x/modules/md
+VPATH        = /Users/sei/projects/httpd/2.4.x/modules/md
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+#   standard stuff
+#
+
+include $(top_srcdir)/build/special.mk
diff --git a/modules/md/Makefile.in b/modules/md/Makefile.in
new file mode 100644 (file)
index 0000000..4395bc3
--- /dev/null
@@ -0,0 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#
+#   standard stuff
+#
+
+include $(top_srcdir)/build/special.mk
diff --git a/modules/md/config2.m4 b/modules/md/config2.m4
new file mode 100644 (file)
index 0000000..a5e1e3e
--- /dev/null
@@ -0,0 +1,293 @@
+dnl Licensed to the Apache Software Foundation (ASF) under one or more
+dnl contributor license agreements.  See the NOTICE file distributed with
+dnl this work for additional information regarding copyright ownership.
+dnl The ASF licenses this file to You under the Apache License, Version 2.0
+dnl (the "License"); you may not use this file except in compliance with
+dnl the License.  You may obtain a copy of the License at
+dnl
+dnl      http://www.apache.org/licenses/LICENSE-2.0
+dnl
+dnl Unless required by applicable law or agreed to in writing, software
+dnl distributed under the License is distributed on an "AS IS" BASIS,
+dnl WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+dnl See the License for the specific language governing permissions and
+dnl limitations under the License.
+
+dnl
+dnl APACHE_CHECK_CURL
+dnl
+dnl Configure for libcurl, giving preference to
+dnl "--with-curl=<path>" if it was specified.
+dnl
+AC_DEFUN([APACHE_CHECK_CURL],[
+  AC_CACHE_CHECK([for curl], [ac_cv_curl], [
+    dnl initialise the variables we use
+    ac_cv_curl=no
+    ap_curl_found=""
+    ap_curl_base=""
+    ap_curl_libs=""
+
+    dnl Determine the curl base directory, if any
+    AC_MSG_CHECKING([for user-provided curl base directory])
+    AC_ARG_WITH(curl, APACHE_HELP_STRING(--with-curl=PATH, curl installation directory), [
+      dnl If --with-curl specifies a directory, we use that directory
+      if test "x$withval" != "xyes" -a "x$withval" != "x"; then
+        dnl This ensures $withval is actually a directory and that it is absolute
+        ap_curl_base="`cd $withval ; pwd`"
+      fi
+    ])
+    if test "x$ap_curl_base" = "x"; then
+      AC_MSG_RESULT(none)
+    else
+      AC_MSG_RESULT($ap_curl_base)
+    fi
+
+    dnl Run header and version checks
+    saved_CPPFLAGS="$CPPFLAGS"
+    saved_LIBS="$LIBS"
+    saved_LDFLAGS="$LDFLAGS"
+
+    dnl Before doing anything else, load in pkg-config variables
+    if test -n "$PKGCONFIG"; then
+      saved_PKG_CONFIG_PATH="$PKG_CONFIG_PATH"
+      AC_MSG_CHECKING([for pkg-config along $PKG_CONFIG_PATH])
+      if test "x$ap_curl_base" != "x" ; then
+        if test -f "${ap_curl_base}/lib/pkgconfig/libcurl.pc"; then
+          dnl Ensure that the given path is used by pkg-config too, otherwise
+          dnl the system libcurl.pc might be picked up instead.
+          PKG_CONFIG_PATH="${ap_curl_base}/lib/pkgconfig${PKG_CONFIG_PATH+:}${PKG_CONFIG_PATH}"
+          export PKG_CONFIG_PATH
+        elif test -f "${ap_curl_base}/lib64/pkgconfig/libcurl.pc"; then
+          dnl Ensure that the given path is used by pkg-config too, otherwise
+          dnl the system libcurl.pc might be picked up instead.
+          PKG_CONFIG_PATH="${ap_curl_base}/lib64/pkgconfig${PKG_CONFIG_PATH+:}${PKG_CONFIG_PATH}"
+          export PKG_CONFIG_PATH
+        fi
+      fi
+      AC_ARG_ENABLE(curl-staticlib-deps,APACHE_HELP_STRING(--enable-curl-staticlib-deps,[link mod_md with dependencies of libcurl's static libraries (as indicated by "pkg-config --static"). Must be specified in addition to --enable-md.]), [
+        if test "$enableval" = "yes"; then
+          PKGCONFIG_LIBOPTS="--static"
+        fi
+      ])
+      ap_curl_libs="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-l --silence-errors libcurl`"
+      if test $? -eq 0; then
+        ap_curl_found="yes"
+        pkglookup="`$PKGCONFIG --cflags-only-I libcurl`"
+        APR_ADDTO(CPPFLAGS, [$pkglookup])
+        APR_ADDTO(MOD_CFLAGS, [$pkglookup])
+        pkglookup="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-L libcurl`"
+        APR_ADDTO(LDFLAGS, [$pkglookup])
+        APR_ADDTO(MOD_LDFLAGS, [$pkglookup])
+        pkglookup="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-other libcurl`"
+        APR_ADDTO(LDFLAGS, [$pkglookup])
+        APR_ADDTO(MOD_LDFLAGS, [$pkglookup])
+      fi
+      PKG_CONFIG_PATH="$saved_PKG_CONFIG_PATH"
+    fi
+
+    dnl fall back to the user-supplied directory if not found via pkg-config
+    if test "x$ap_curl_base" != "x" -a "x$ap_curl_found" = "x"; then
+      APR_ADDTO(CPPFLAGS, [-I$ap_curl_base/include])
+      APR_ADDTO(MOD_CFLAGS, [-I$ap_curl_base/include])
+      APR_ADDTO(LDFLAGS, [-L$ap_curl_base/lib])
+      APR_ADDTO(MOD_LDFLAGS, [-L$ap_curl_base/lib])
+      if test "x$ap_platform_runtime_link_flag" != "x"; then
+        APR_ADDTO(LDFLAGS, [$ap_platform_runtime_link_flag$ap_curl_base/lib])
+        APR_ADDTO(MOD_LDFLAGS, [$ap_platform_runtime_link_flag$ap_curl_base/lib])
+      fi
+    fi
+
+    AC_CHECK_HEADERS([curl/curl.h])
+
+    AC_MSG_CHECKING([for curl version >= 7.50])
+    AC_TRY_COMPILE([#include <curl/curlver.h>],[
+#if !defined(LIBCURL_VERSION_MAJOR)
+#error "Missing libcurl version"
+#endif
+#if LIBCURL_VERSION_MAJOR < 7
+#error "Unsupported libcurl version " LIBCURL_VERSION
+#endif
+#if LIBCURL_VERSION_MAJOR == 7 && LIBCURL_VERSION_MINOR < 50
+#error "Unsupported libcurl version " LIBCURL_VERSION
+#endif],
+      [AC_MSG_RESULT(OK)
+       ac_cv_curl=yes],
+      [AC_MSG_RESULT(FAILED)])
+
+    if test "x$ac_cv_curl" = "xyes"; then
+      ap_curl_libs="${ap_curl_libs:--lcurl} `$apr_config --libs`"
+      APR_ADDTO(MOD_LDFLAGS, [$ap_curl_libs])
+      APR_ADDTO(LIBS, [$ap_curl_libs])
+    fi
+
+    dnl restore
+    CPPFLAGS="$saved_CPPFLAGS"
+    LIBS="$saved_LIBS"
+    LDFLAGS="$saved_LDFLAGS"
+  ])
+  if test "x$ac_cv_curl" = "xyes"; then
+    AC_DEFINE(HAVE_CURL, 1, [Define if curl is available])
+  fi
+])
+
+
+dnl
+dnl APACHE_CHECK_JANSSON
+dnl
+dnl Configure for libjansson, giving preference to
+dnl "--with-jansson=<path>" if it was specified.
+dnl
+AC_DEFUN([APACHE_CHECK_JANSSON],[
+  AC_CACHE_CHECK([for jansson], [ac_cv_jansson], [
+    dnl initialise the variables we use
+    ac_cv_jansson=no
+    ap_jansson_found=""
+    ap_jansson_base=""
+    ap_jansson_libs=""
+
+    dnl Determine the jansson base directory, if any
+    AC_MSG_CHECKING([for user-provided jansson base directory])
+    AC_ARG_WITH(jansson, APACHE_HELP_STRING(--with-jansson=PATH, jansson installation directory), [
+      dnl If --with-jansson specifies a directory, we use that directory
+      if test "x$withval" != "xyes" -a "x$withval" != "x"; then
+        dnl This ensures $withval is actually a directory and that it is absolute
+        ap_jansson_base="`cd $withval ; pwd`"
+      fi
+    ])
+    if test "x$ap_jansson_base" = "x"; then
+      AC_MSG_RESULT(none)
+    else
+      AC_MSG_RESULT($ap_jansson_base)
+    fi
+
+    dnl Run header and version checks
+    saved_CPPFLAGS="$CPPFLAGS"
+    saved_LIBS="$LIBS"
+    saved_LDFLAGS="$LDFLAGS"
+
+    dnl Before doing anything else, load in pkg-config variables
+    if test -n "$PKGCONFIG"; then
+      saved_PKG_CONFIG_PATH="$PKG_CONFIG_PATH"
+      AC_MSG_CHECKING([for pkg-config along $PKG_CONFIG_PATH])
+      if test "x$ap_jansson_base" != "x" ; then
+        if test -f "${ap_jansson_base}/lib/pkgconfig/libjansson.pc"; then
+          dnl Ensure that the given path is used by pkg-config too, otherwise
+          dnl the system libjansson.pc might be picked up instead.
+          PKG_CONFIG_PATH="${ap_jansson_base}/lib/pkgconfig${PKG_CONFIG_PATH+:}${PKG_CONFIG_PATH}"
+          export PKG_CONFIG_PATH
+        elif test -f "${ap_jansson_base}/lib64/pkgconfig/libjansson.pc"; then
+          dnl Ensure that the given path is used by pkg-config too, otherwise
+          dnl the system libjansson.pc might be picked up instead.
+          PKG_CONFIG_PATH="${ap_jansson_base}/lib64/pkgconfig${PKG_CONFIG_PATH+:}${PKG_CONFIG_PATH}"
+          export PKG_CONFIG_PATH
+        fi
+      fi
+      AC_ARG_ENABLE(jansson-staticlib-deps,APACHE_HELP_STRING(--enable-jansson-staticlib-deps,[link mod_md with dependencies of libjansson's static libraries (as indicated by "pkg-config --static"). Must be specified in addition to --enable-md.]), [
+        if test "$enableval" = "yes"; then
+          PKGCONFIG_LIBOPTS="--static"
+        fi
+      ])
+      ap_jansson_libs="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-l --silence-errors libjansson`"
+      if test $? -eq 0; then
+        ap_jansson_found="yes"
+        pkglookup="`$PKGCONFIG --cflags-only-I libjansson`"
+        APR_ADDTO(CPPFLAGS, [$pkglookup])
+        APR_ADDTO(MOD_CFLAGS, [$pkglookup])
+        pkglookup="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-L libjansson`"
+        APR_ADDTO(LDFLAGS, [$pkglookup])
+        APR_ADDTO(MOD_LDFLAGS, [$pkglookup])
+        pkglookup="`$PKGCONFIG $PKGCONFIG_LIBOPTS --libs-only-other libjansson`"
+        APR_ADDTO(LDFLAGS, [$pkglookup])
+        APR_ADDTO(MOD_LDFLAGS, [$pkglookup])
+      fi
+      PKG_CONFIG_PATH="$saved_PKG_CONFIG_PATH"
+    fi
+
+    dnl fall back to the user-supplied directory if not found via pkg-config
+    if test "x$ap_jansson_base" != "x" -a "x$ap_jansson_found" = "x"; then
+      APR_ADDTO(CPPFLAGS, [-I$ap_jansson_base/include])
+      APR_ADDTO(MOD_CFLAGS, [-I$ap_jansson_base/include])
+      APR_ADDTO(LDFLAGS, [-L$ap_jansson_base/lib])
+      APR_ADDTO(MOD_LDFLAGS, [-L$ap_jansson_base/lib])
+      if test "x$ap_platform_runtime_link_flag" != "x"; then
+        APR_ADDTO(LDFLAGS, [$ap_platform_runtime_link_flag$ap_jansson_base/lib])
+        APR_ADDTO(MOD_LDFLAGS, [$ap_platform_runtime_link_flag$ap_jansson_base/lib])
+      fi
+    fi
+
+    # attempts to include jansson.h fail me. So lets make sure we can at least
+    # include its other header file
+    AC_TRY_COMPILE([#include <jansson_config.h>],[],
+      [AC_MSG_RESULT(OK) 
+       ac_cv_jansson=yes], 
+       [AC_MSG_RESULT(FAILED)])
+
+    if test "x$ac_cv_jansson" = "xyes"; then
+      ap_jansson_libs="${ap_jansson_libs:--ljansson} `$apr_config --libs`"
+      APR_ADDTO(MOD_LDFLAGS, [$ap_jansson_libs])
+      APR_ADDTO(LIBS, [$ap_jansson_libs])
+    fi
+
+    dnl restore
+    CPPFLAGS="$saved_CPPFLAGS"
+    LIBS="$saved_LIBS"
+    LDFLAGS="$saved_LDFLAGS"
+  ])
+  if test "x$ac_cv_jansson" = "xyes"; then
+    AC_DEFINE(HAVE_JANSSON, 1, [Define if jansson is available])
+  fi
+])
+
+
+dnl #  start of module specific part
+APACHE_MODPATH_INIT(md)
+
+dnl #  list of module object files
+md_objs="dnl
+md_acme.lo dnl
+md_acme_acct.lo dnl
+md_acme_authz.lo dnl
+md_acme_drive.lo dnl
+md_core.lo dnl
+md_curl.lo dnl
+md_crypt.lo dnl
+md_http.lo dnl
+md_json.lo dnl
+md_jws.lo dnl
+md_log.lo dnl
+md_reg.lo dnl
+md_store.lo dnl
+md_store_fs.lo dnl
+md_util.lo dnl
+mod_md.lo dnl
+mod_md_config.lo dnl
+mod_md_os.lo dnl
+"
+
+# Ensure that other modules can pick up mod_md.h
+APR_ADDTO(INCLUDES, [-I\$(top_srcdir)/$modpath_current])
+
+dnl # hook module into the Autoconf mechanism (--enable-md)
+APACHE_MODULE(md, [Managed Domain handling], $md_objs, , most, [
+    APACHE_CHECK_OPENSSL
+    if test "x$ac_cv_openssl" = "xno" ; then
+        AC_MSG_WARN([libssl (or compatible) not found])
+        enable_md=no
+    fi
+    
+    APACHE_CHECK_JANSSON
+    if test "x$ac_cv_jansson" != "xyes" ; then
+        AC_MSG_WARN([libjansson not found])
+        enable_md=no
+    fi
+
+    APACHE_CHECK_CURL
+    if test "x$ac_cv_curl" != "xyes" ; then
+        AC_MSG_WARN([libcurl not found])
+        enable_md=no
+    fi
+])
+
+dnl #  end of module specific part
+APACHE_MODPATH_FINISH
+
diff --git a/modules/md/md.h b/modules/md/md.h
new file mode 100644 (file)
index 0000000..0c4aed5
--- /dev/null
@@ -0,0 +1,278 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef mod_md_md_h
+#define mod_md_md_h
+
+#include "md_version.h"
+
+struct apr_array_header_t;
+struct apr_hash_t;
+struct md_json_t;
+struct md_cert_t;
+struct md_pkey_t;
+struct md_store_t;
+struct md_srv_conf_t;
+struct md_pkey_spec_t;
+
+#define MD_TLSSNI01_DNS_SUFFIX     ".acme.invalid"
+
+#define MD_PKEY_RSA_BITS_MIN       2048
+#define MD_PKEY_RSA_BITS_DEF       2048
+
+/* Minimum age for the HSTS header (RFC 6797), considered appropriate by Mozilla Security */
+#define MD_HSTS_HEADER             "Strict-Transport-Security"
+#define MD_HSTS_MAX_AGE_DEFAULT    15768000
+
+typedef enum {
+    MD_S_UNKNOWN,                   /* MD has not been analysed yet */
+    MD_S_INCOMPLETE,                /* MD is missing necessary information, cannot go live */
+    MD_S_COMPLETE,                  /* MD has all necessary information, can go live */
+    MD_S_EXPIRED,                   /* MD is complete, but credentials have expired */
+    MD_S_ERROR,                     /* MD data is flawed, unable to be processed as is */ 
+    MD_S_MISSING,                   /* MD is missing config information, cannot proceed */
+} md_state_t;
+
+typedef enum {
+    MD_REQUIRE_UNSET = -1,
+    MD_REQUIRE_OFF,
+    MD_REQUIRE_TEMPORARY,
+    MD_REQUIRE_PERMANENT,
+} md_require_t;
+
+typedef enum {
+    MD_SV_TEXT,
+    MD_SV_JSON,
+    MD_SV_CERT,
+    MD_SV_PKEY,
+    MD_SV_CHAIN,
+} md_store_vtype_t;
+
+typedef enum {
+    MD_SG_NONE,
+    MD_SG_ACCOUNTS,
+    MD_SG_CHALLENGES,
+    MD_SG_DOMAINS,
+    MD_SG_STAGING,
+    MD_SG_ARCHIVE,
+    MD_SG_TMP,
+    MD_SG_COUNT,
+} md_store_group_t;
+
+typedef enum {
+    MD_DRIVE_DEFAULT = -1,          /* default value */
+    MD_DRIVE_MANUAL,                /* manually triggered transmission of credentials */
+    MD_DRIVE_AUTO,                  /* automatic process performed by httpd */
+    MD_DRIVE_ALWAYS,                /* always driven by httpd, even if not used in any vhost */
+} md_drive_mode_t;
+
+typedef struct md_t md_t;
+struct md_t {
+    const char *name;               /* unique name of this MD */
+    struct apr_array_header_t *domains; /* all DNS names this MD includes */
+    struct apr_array_header_t *contacts;   /* list of contact uris, e.g. mailto:xxx */
+
+    int transitive;                 /* != 0 iff VirtualHost names/aliases are auto-added */
+    md_require_t require_https;     /* Iff https: is required for this MD */
+    
+    int drive_mode;                 /* mode of obtaining credentials */
+    struct md_pkey_spec_t *pkey_spec;/* specification for generating new private keys */
+    int must_staple;                /* certificates should set the OCSP Must Staple extension */
+    apr_interval_time_t renew_norm; /* if > 0, normalized cert lifetime */
+    apr_interval_time_t renew_window;/* time before expiration that starts renewal */
+    
+    const char *ca_url;             /* url of CA certificate service */
+    const char *ca_proto;           /* protocol used vs CA (e.g. ACME) */
+    const char *ca_account;         /* account used at CA */
+    const char *ca_agreement;       /* accepted agreement uri between CA and user */ 
+    struct apr_array_header_t *ca_challenges; /* challenge types configured for this MD */
+
+    md_state_t state;               /* state of this MD */
+    apr_time_t valid_from;          /* When the credentials start to be valid. 0 if unknown */
+    apr_time_t expires;             /* When the credentials expire. 0 if unknown */
+    const char *cert_url;           /* url where cert has been created, remember during drive */ 
+    
+    const struct md_srv_conf_t *sc; /* server config where it was defined or NULL */
+    const char *defn_name;          /* config file this MD was defined */
+    unsigned defn_line_number;      /* line number of definition */
+};
+
+#define MD_KEY_ACCOUNT          "account"
+#define MD_KEY_AGREEMENT        "agreement"
+#define MD_KEY_BITS             "bits"
+#define MD_KEY_CA               "ca"
+#define MD_KEY_CA_URL           "ca-url"
+#define MD_KEY_CERT             "cert"
+#define MD_KEY_CHALLENGES       "challenges"
+#define MD_KEY_CONTACT          "contact"
+#define MD_KEY_CONTACTS         "contacts"
+#define MD_KEY_CSR              "csr"
+#define MD_KEY_DISABLED         "disabled"
+#define MD_KEY_DIR              "dir"
+#define MD_KEY_DOMAIN           "domain"
+#define MD_KEY_DOMAINS          "domains"
+#define MD_KEY_DRIVE_MODE       "drive-mode"
+#define MD_KEY_ERRORS           "errors"
+#define MD_KEY_EXPIRES          "expires"
+#define MD_KEY_HTTP             "http"
+#define MD_KEY_HTTPS            "https"
+#define MD_KEY_ID               "id"
+#define MD_KEY_IDENTIFIER       "identifier"
+#define MD_KEY_KEY              "key"
+#define MD_KEY_KEYAUTHZ         "keyAuthorization"
+#define MD_KEY_LOCATION         "location"
+#define MD_KEY_MUST_STAPLE      "must-staple"
+#define MD_KEY_NAME             "name"
+#define MD_KEY_PERMANENT        "permanent"
+#define MD_KEY_PKEY             "privkey"
+#define MD_KEY_PROCESSED        "processed"
+#define MD_KEY_PROTO            "proto"
+#define MD_KEY_REGISTRATION     "registration"
+#define MD_KEY_RENEW            "renew"
+#define MD_KEY_RENEW_WINDOW     "renew-window"
+#define MD_KEY_REQUIRE_HTTPS    "require-https"
+#define MD_KEY_RESOURCE         "resource"
+#define MD_KEY_STATE            "state"
+#define MD_KEY_STATUS           "status"
+#define MD_KEY_STORE            "store"
+#define MD_KEY_TEMPORARY        "temporary"
+#define MD_KEY_TOKEN            "token"
+#define MD_KEY_TRANSITIVE       "transitive"
+#define MD_KEY_TYPE             "type"
+#define MD_KEY_URL              "url"
+#define MD_KEY_URI              "uri"
+#define MD_KEY_VALID_FROM       "validFrom"
+#define MD_KEY_VALUE            "value"
+#define MD_KEY_VERSION          "version"
+
+#define MD_FN_MD                "md.json"
+#define MD_FN_JOB               "job.json"
+#define MD_FN_PRIVKEY           "privkey.pem"
+#define MD_FN_PUBCERT           "pubcert.pem"
+#define MD_FN_CERT              "cert.pem"
+#define MD_FN_CHAIN             "chain.pem"
+#define MD_FN_HTTPD_JSON        "httpd.json"
+
+#define MD_FN_FALLBACK_PKEY     "fallback-privkey.pem"
+#define MD_FN_FALLBACK_CERT     "fallback-cert.pem"
+
+/* Check if a string member of a new MD (n) has 
+ * a value and if it differs from the old MD o
+ */
+#define MD_VAL_UPDATE(n,o,s)    ((n)->s != (o)->s)
+#define MD_SVAL_UPDATE(n,o,s)   ((n)->s && (!(o)->s || strcmp((n)->s, (o)->s)))
+
+/**
+ * Determine if the Managed Domain contains a specific domain name.
+ */
+int md_contains(const md_t *md, const char *domain, int case_sensitive);
+
+/**
+ * Determine if the names of the two managed domains overlap.
+ */
+int md_domains_overlap(const md_t *md1, const md_t *md2);
+
+/**
+ * Determine if the domain names are equal.
+ */
+int md_equal_domains(const md_t *md1, const md_t *md2, int case_sensitive);
+
+/**
+ * Determine if the domains in md1 contain all domains of md2.
+ */
+int md_contains_domains(const md_t *md1, const md_t *md2);
+
+/**
+ * Get one common domain name of the two managed domains or NULL.
+ */
+const char *md_common_name(const md_t *md1, const md_t *md2);
+
+/**
+ * Get the number of common domains.
+ */
+apr_size_t md_common_name_count(const md_t *md1, const md_t *md2);
+
+/**
+ * Look up a managed domain by its name.
+ */
+md_t *md_get_by_name(struct apr_array_header_t *mds, const char *name);
+
+/**
+ * Look up a managed domain by a DNS name it contains.
+ */
+md_t *md_get_by_domain(struct apr_array_header_t *mds, const char *domain);
+
+/**
+ * Find a managed domain, different from the given one, that has overlaps
+ * in the domain list.
+ */
+md_t *md_get_by_dns_overlap(struct apr_array_header_t *mds, const md_t *md);
+
+/**
+ * Find the managed domain in the list that, for the given md, 
+ * has the same name, or the most number of overlaps in domains
+ */
+md_t *md_find_closest_match(apr_array_header_t *mds, const md_t *md);
+
+/**
+ * Create and empty md record, structures initialized.
+ */
+md_t *md_create_empty(apr_pool_t *p);
+
+/**
+ * Create a managed domain, given a list of domain names.
+ */
+md_t *md_create(apr_pool_t *p, struct apr_array_header_t *domains);
+
+/**
+ * Deep copy an md record into another pool.
+ */
+md_t *md_clone(apr_pool_t *p, const md_t *src);
+
+/**
+ * Shallow copy an md record into another pool.
+ */
+md_t *md_copy(apr_pool_t *p, const md_t *src);
+
+/**
+ * Create a merged md with the settings of add overlaying the ones from base.
+ */
+md_t *md_merge(apr_pool_t *p, const md_t *add, const md_t *base);
+
+/** 
+ * Convert the managed domain into a JSON representation and vice versa. 
+ *
+ * This reads and writes the following information: name, domains, ca_url, ca_proto and state.
+ */
+struct md_json_t *md_to_json (const md_t *md, apr_pool_t *p);
+md_t *md_from_json(struct md_json_t *json, apr_pool_t *p);
+
+/**
+ * Determine if MD should renew its cert (if it has one)
+ */
+int md_should_renew(const md_t *md);
+
+/**************************************************************************************************/
+/* domain credentials */
+
+typedef struct md_creds_t md_creds_t;
+struct md_creds_t {
+    struct md_pkey_t *privkey;
+    struct apr_array_header_t *pubcert;    /* complete md_cert* chain */
+    struct md_cert_t *cert;
+    int expired;
+};
+
+#endif /* mod_md_md_h */
diff --git a/modules/md/md_acme.c b/modules/md/md_acme.c
new file mode 100644 (file)
index 0000000..90bdf3c
--- /dev/null
@@ -0,0 +1,516 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+#include <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_buckets.h>
+#include <apr_hash.h>
+#include <apr_uri.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_jws.h"
+#include "md_http.h"
+#include "md_log.h"
+#include "md_store.h"
+#include "md_util.h"
+#include "md_version.h"
+
+#include "md_acme.h"
+#include "md_acme_acct.h"
+
+
+static const char *base_product;
+
+typedef struct acme_problem_status_t acme_problem_status_t;
+
+struct acme_problem_status_t {
+    const char *type;
+    apr_status_t rv;
+};
+
+static acme_problem_status_t Problems[] = {
+    { "acme:error:badCSR",                       APR_EINVAL },
+    { "acme:error:badNonce",                     APR_EAGAIN },
+    { "acme:error:badSignatureAlgorithm",        APR_EINVAL },
+    { "acme:error:invalidContact",               APR_BADARG },
+    { "acme:error:unsupportedContact",           APR_EGENERAL },
+    { "acme:error:malformed",                    APR_EINVAL },
+    { "acme:error:rateLimited",                  APR_BADARG },
+    { "acme:error:rejectedIdentifier",           APR_BADARG },
+    { "acme:error:serverInternal",               APR_EGENERAL },
+    { "acme:error:unauthorized",                 APR_EACCES },
+    { "acme:error:unsupportedIdentifier",        APR_BADARG },
+    { "acme:error:userActionRequired",           APR_EAGAIN },
+    { "acme:error:badRevocationReason",          APR_EINVAL },
+    { "acme:error:caa",                          APR_EGENERAL },
+    { "acme:error:dns",                          APR_EGENERAL },
+    { "acme:error:connection",                   APR_EGENERAL },
+    { "acme:error:tls",                          APR_EGENERAL },
+    { "acme:error:incorrectResponse",            APR_EGENERAL },
+};
+
+static apr_status_t problem_status_get(const char *type) {
+    size_t i;
+
+    if (strstr(type, "urn:ietf:params:") == type) {
+        type += strlen("urn:ietf:params:");
+    }
+    else if (strstr(type, "urn:") == type) {
+        type += strlen("urn:");
+    }
+     
+    for(i = 0; i < (sizeof(Problems)/sizeof(Problems[0])); ++i) {
+        if (!apr_strnatcasecmp(type, Problems[i].type)) {
+            return Problems[i].rv;
+        }
+    }
+    return APR_EGENERAL;
+}
+
+apr_status_t md_acme_init(apr_pool_t *p, const char *base)
+{
+    base_product = base;
+    return md_crypt_init(p);
+}
+
+apr_status_t md_acme_create(md_acme_t **pacme, apr_pool_t *p, const char *url,
+                            const char *proxy_url)
+{
+    md_acme_t *acme;
+    const char *err = NULL;
+    apr_status_t rv;
+    apr_uri_t uri_parsed;
+    size_t len;
+    
+    if (!url) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p, "create ACME without url");
+        return APR_EINVAL;
+    }
+    
+    if (APR_SUCCESS != (rv = md_util_abs_uri_check(p, url, &err))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "invalid ACME uri ($s): %s", err, url);
+        return rv;
+    }
+    
+    acme = apr_pcalloc(p, sizeof(*acme));
+    acme->url = url;
+    acme->p = p;
+    acme->user_agent = apr_psprintf(p, "%s mod_md/%s", 
+                                    base_product, MOD_MD_VERSION);
+    acme->proxy_url = proxy_url? apr_pstrdup(p, proxy_url) : NULL;
+    acme->max_retries = 3;
+    
+    if (APR_SUCCESS != (rv = apr_uri_parse(p, url, &uri_parsed))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "parsing ACME uri: ", url);
+        return APR_EINVAL;
+    }
+    
+    len = strlen(uri_parsed.hostname);
+    acme->sname = (len <= 16)? uri_parsed.hostname : apr_pstrdup(p, uri_parsed.hostname + len - 16);
+    
+    *pacme = (APR_SUCCESS == rv)? acme : NULL;
+    return rv;
+}
+
+apr_status_t md_acme_setup(md_acme_t *acme)
+{
+    apr_status_t rv;
+    md_json_t *json;
+    
+    assert(acme->url);
+    if (!acme->http && APR_SUCCESS != (rv = md_http_create(&acme->http, acme->p,
+                                                           acme->user_agent, acme->proxy_url))) {
+        return rv;
+    }
+    md_http_set_response_limit(acme->http, 1024*1024);
+    
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "get directory from %s", acme->url);
+    
+    rv = md_acme_get_json(&json, acme, acme->url, acme->p);
+    if (APR_SUCCESS == rv) {
+        acme->new_authz = md_json_gets(json, "new-authz", NULL);
+        acme->new_cert = md_json_gets(json, "new-cert", NULL);
+        acme->new_reg = md_json_gets(json, "new-reg", NULL);
+        acme->revoke_cert = md_json_gets(json, "revoke-cert", NULL);
+        if (acme->new_authz && acme->new_cert && acme->new_reg && acme->revoke_cert) {
+            return APR_SUCCESS;
+        }
+        rv = APR_EINVAL;
+    }
+    return rv;
+}
+
+/**************************************************************************************************/
+/* acme requests */
+
+static void req_update_nonce(md_acme_t *acme, apr_table_t *hdrs)
+{
+    if (hdrs) {
+        const char *nonce = apr_table_get(hdrs, "Replay-Nonce");
+        if (nonce) {
+            acme->nonce = apr_pstrdup(acme->p, nonce);
+        }
+    }
+}
+
+static apr_status_t http_update_nonce(const md_http_response_t *res)
+{
+    if (res->headers) {
+        const char *nonce = apr_table_get(res->headers, "Replay-Nonce");
+        if (nonce) {
+            md_acme_t *acme = res->req->baton;
+            acme->nonce = apr_pstrdup(acme->p, nonce);
+        }
+    }
+    return res->rv;
+}
+
+static apr_status_t md_acme_new_nonce(md_acme_t *acme)
+{
+    apr_status_t rv;
+    long id;
+    
+    rv = md_http_HEAD(acme->http, acme->new_reg, NULL, http_update_nonce, acme, &id);
+    md_http_await(acme->http, id);
+    return rv;
+}
+
+static md_acme_req_t *md_acme_req_create(md_acme_t *acme, const char *method, const char *url)
+{
+    apr_pool_t *pool;
+    md_acme_req_t *req;
+    apr_status_t rv;
+    
+    rv = apr_pool_create(&pool, acme->p);
+    if (rv != APR_SUCCESS) {
+        return NULL;
+    }
+    
+    req = apr_pcalloc(pool, sizeof(*req));
+    if (!req) {
+        apr_pool_destroy(pool);
+        return NULL;
+    }
+        
+    req->acme = acme;
+    req->p = pool;
+    req->method = method;
+    req->url = url;
+    req->prot_hdrs = apr_table_make(pool, 5);
+    if (!req->prot_hdrs) {
+        apr_pool_destroy(pool);
+        return NULL;
+    }
+    req->max_retries = acme->max_retries;
+    
+    return req;
+}
+apr_status_t md_acme_req_body_init(md_acme_req_t *req, md_json_t *jpayload)
+{
+    const char *payload;
+    size_t payload_len;
+
+    if (!req->acme->acct) {
+        return APR_EINVAL;
+    }
+
+    payload = md_json_writep(jpayload, req->p, MD_JSON_FMT_COMPACT);
+    if (!payload) {
+        return APR_EINVAL;
+    }
+
+    payload_len = strlen(payload);
+    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, req->p, 
+                  "acct payload(len=%d): %s", payload_len, payload);
+    return md_jws_sign(&req->req_json, req->p, payload, payload_len,
+                       req->prot_hdrs, req->acme->acct_key, NULL);
+} 
+
+
+static apr_status_t inspect_problem(md_acme_req_t *req, const md_http_response_t *res)
+{
+    const char *ctype;
+    md_json_t *problem;
+    
+    ctype = apr_table_get(req->resp_hdrs, "content-type");
+    if (ctype && !strcmp(ctype, "application/problem+json")) {
+        /* RFC 7807 */
+        md_json_read_http(&problem, req->p, res);
+        if (problem) {
+            const char *ptype, *pdetail;
+            
+            req->resp_json = problem;
+            ptype = md_json_gets(problem, "type", NULL); 
+            pdetail = md_json_gets(problem, "detail", NULL);
+            req->rv = problem_status_get(ptype);
+            
+            if (APR_STATUS_IS_EAGAIN(req->rv)) {
+                md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, req->rv, req->p,
+                              "acme reports %s: %s", ptype, pdetail);
+            }
+            else {
+                md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, req->rv, req->p,
+                              "acme problem %s: %s", ptype, pdetail);
+            }
+            return req->rv;
+        }
+    }
+    
+    if (APR_SUCCESS == res->rv) {
+        switch (res->status) {
+            case 400:
+                return APR_EINVAL;
+            case 403:
+                return APR_EACCES;
+            case 404:
+                return APR_ENOENT;
+            default:
+                md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, req->p,
+                              "acme problem unknown: http status %d", res->status);
+                return APR_EGENERAL;
+        }
+    }
+    return res->rv;
+}
+
+/**************************************************************************************************/
+/* ACME requests with nonce handling */
+
+static apr_status_t md_acme_req_done(md_acme_req_t *req)
+{
+    apr_status_t rv = req->rv;
+    if (req->p) {
+        apr_pool_destroy(req->p);
+    }
+    return rv;
+}
+
+static apr_status_t on_response(const md_http_response_t *res)
+{
+    md_acme_req_t *req = res->req->baton;
+    apr_status_t rv = res->rv;
+    
+    if (APR_SUCCESS != rv) {
+        goto out;
+    }
+    
+    req->resp_hdrs = apr_table_clone(req->p, res->headers);
+    req_update_nonce(req->acme, res->headers);
+    
+    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, req->p, "response: %d", res->status);
+    if (res->status >= 200 && res->status < 300) {
+        int processed = 0;
+        
+        if (req->on_json) {
+            processed = 1;
+            rv = md_json_read_http(&req->resp_json, req->p, res);
+            if (APR_SUCCESS == rv) {
+                if (md_log_is_level(req->p, MD_LOG_TRACE2)) {
+                    const char *s;
+                    s = md_json_writep(req->resp_json, req->p, MD_JSON_FMT_INDENT);
+                    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, req->p,
+                                  "response: %s",
+                                  s ? s : "<failed to serialize!>");
+                }
+                rv = req->on_json(req->acme, req->p, req->resp_hdrs, req->resp_json, req->baton);
+            }        
+            else if (APR_STATUS_IS_ENOENT(rv)) {
+                /* not JSON content, fall through */
+                processed = 0;
+            }
+            else {
+                md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, req->p, "parsing JSON body");
+            }
+        }
+        
+        if (!processed && req->on_res) {
+            processed = 1;
+            rv = req->on_res(req->acme, res, req->baton);
+        }
+        
+        if (!processed) {
+            rv = APR_EINVAL;
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, req->p, 
+                          "response: %d, content-type=%s", res->status, 
+                          apr_table_get(res->headers, "Content-Type"));
+        }
+    }
+    else if (APR_EAGAIN == (rv = inspect_problem(req, res))) {
+        /* leave req alive */
+        return rv;
+    }
+
+out:
+    md_acme_req_done(req);
+    return rv;
+}
+
+static apr_status_t md_acme_req_send(md_acme_req_t *req)
+{
+    apr_status_t rv;
+    md_acme_t *acme = req->acme;
+    const char *body = NULL;
+
+    assert(acme->url);
+    
+    if (strcmp("GET", req->method) && strcmp("HEAD", req->method)) {
+        if (!acme->new_authz) {
+            if (APR_SUCCESS != (rv = md_acme_setup(acme))) {
+                return rv;
+            }
+        }
+        if (!acme->nonce) {
+            if (APR_SUCCESS != (rv = md_acme_new_nonce(acme))) {
+                return rv;
+            }
+        }
+        
+        apr_table_set(req->prot_hdrs, "nonce", acme->nonce);
+        acme->nonce = NULL;
+    }
+    
+    rv = req->on_init? req->on_init(req, req->baton) : APR_SUCCESS;
+    
+    if ((rv == APR_SUCCESS) && req->req_json) {
+        body = md_json_writep(req->req_json, req->p, MD_JSON_FMT_INDENT);
+        if (!body) {
+            rv = APR_EINVAL;
+        }
+    }
+
+    if (rv == APR_SUCCESS) {
+        long id = 0;
+        
+        if (body && md_log_is_level(req->p, MD_LOG_TRACE2)) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, req->p, 
+                          "req: POST %s, body:\n%s", req->url, body);
+        }
+        else {
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, req->p, 
+                          "req: POST %s", req->url);
+        }
+        if (!strcmp("GET", req->method)) {
+            rv = md_http_GET(req->acme->http, req->url, NULL, on_response, req, &id);
+        }
+        else if (!strcmp("POST", req->method)) {
+            rv = md_http_POSTd(req->acme->http, req->url, NULL, "application/json",  
+                               body, body? strlen(body) : 0, on_response, req, &id);
+        }
+        else if (!strcmp("HEAD", req->method)) {
+            rv = md_http_HEAD(req->acme->http, req->url, NULL, on_response, req, &id);
+        }
+        else {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, req->p, 
+                          "HTTP method %s against: %s", req->method, req->url);
+            rv = APR_ENOTIMPL;
+        }
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, req->p, "req sent");
+        md_http_await(acme->http, id);
+        
+        if (APR_EAGAIN == rv && req->max_retries > 0) {
+            --req->max_retries;
+            return md_acme_req_send(req);
+        }
+        req = NULL;
+    }
+
+    if (req) {
+        md_acme_req_done(req);
+    }
+    return rv;
+}
+
+apr_status_t md_acme_POST(md_acme_t *acme, const char *url,
+                          md_acme_req_init_cb *on_init,
+                          md_acme_req_json_cb *on_json,
+                          md_acme_req_res_cb *on_res,
+                          void *baton)
+{
+    md_acme_req_t *req;
+    
+    assert(url);
+    assert(on_json || on_res);
+
+    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, acme->p, "add acme POST: %s", url);
+    req = md_acme_req_create(acme, "POST", url);
+    req->on_init = on_init;
+    req->on_json = on_json;
+    req->on_res = on_res;
+    req->baton = baton;
+    
+    return md_acme_req_send(req);
+}
+
+apr_status_t md_acme_GET(md_acme_t *acme, const char *url,
+                         md_acme_req_init_cb *on_init,
+                         md_acme_req_json_cb *on_json,
+                         md_acme_req_res_cb *on_res,
+                         void *baton)
+{
+    md_acme_req_t *req;
+    
+    assert(url);
+    assert(on_json || on_res);
+
+    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, acme->p, "add acme GET: %s", url);
+    req = md_acme_req_create(acme, "GET", url);
+    req->on_init = on_init;
+    req->on_json = on_json;
+    req->on_res = on_res;
+    req->baton = baton;
+    
+    return md_acme_req_send(req);
+}
+
+/**************************************************************************************************/
+/* GET JSON */
+
+typedef struct {
+    apr_pool_t *pool;
+    md_json_t *json;
+} json_ctx;
+
+static apr_status_t on_got_json(md_acme_t *acme, apr_pool_t *p, const apr_table_t *headers, 
+                                md_json_t *jbody, void *baton)
+{
+    json_ctx *ctx = baton;
+
+    (void)acme;
+    (void)p;
+    (void)headers;
+    ctx->json = md_json_clone(ctx->pool, jbody);
+    return APR_SUCCESS;
+}
+
+apr_status_t md_acme_get_json(struct md_json_t **pjson, md_acme_t *acme, 
+                              const char *url, apr_pool_t *p)
+{
+    apr_status_t rv;
+    json_ctx ctx;
+    
+    ctx.pool = p;
+    ctx.json = NULL;
+    
+    rv = md_acme_GET(acme, url, NULL, on_got_json, NULL, &ctx);
+    *pjson = (APR_SUCCESS == rv)? ctx.json : NULL;
+    return rv;
+}
+
diff --git a/modules/md/md_acme.h b/modules/md/md_acme.h
new file mode 100644 (file)
index 0000000..30dfbbc
--- /dev/null
@@ -0,0 +1,266 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef mod_md_md_acme_h
+#define mod_md_md_acme_h
+
+struct apr_array_header_t;
+struct apr_bucket_brigade;
+struct md_http_response_t;
+struct apr_hash_t;
+struct md_http_t;
+struct md_json_t;
+struct md_pkey_t;
+struct md_t;
+struct md_acme_acct_t;
+struct md_proto_t;
+struct md_store_t;
+
+#define MD_PROTO_ACME               "ACME"
+
+#define MD_AUTHZ_CHA_HTTP_01        "http-01"
+#define MD_AUTHZ_CHA_SNI_01         "tls-sni-01"
+
+typedef enum {
+    MD_ACME_S_UNKNOWN,              /* MD has not been analysed yet */
+    MD_ACME_S_REGISTERED,           /* MD is registered at CA, but not more */
+    MD_ACME_S_TOS_ACCEPTED,         /* Terms of Service were accepted by account holder */
+    MD_ACME_S_CHALLENGED,           /* MD challenge information for all domains is known */
+    MD_ACME_S_VALIDATED,            /* MD domains have been validated */
+    MD_ACME_S_CERTIFIED,            /* MD has valid certificate */
+    MD_ACME_S_DENIED,               /* MD domains (at least one) have been denied by CA */
+} md_acme_state_t;
+
+typedef struct md_acme_t md_acme_t;
+
+struct md_acme_t {
+    const char *url;                /* directory url of the ACME service */
+    const char *sname;              /* short name for the service, not necessarily unique */
+    apr_pool_t *p;
+    const char *user_agent;
+    const char *proxy_url;
+    struct md_acme_acct_t *acct;
+    struct md_pkey_t *acct_key;
+    
+    const char *new_authz;
+    const char *new_cert;
+    const char *new_reg;
+    const char *revoke_cert;
+    
+    struct md_http_t *http;
+    
+    const char *nonce;
+    int max_retries;
+};
+
+/**
+ * Global init, call once at start up.
+ */
+apr_status_t md_acme_init(apr_pool_t *pool, const char *base_version);
+
+/**
+ * Create a new ACME server instance. If path is not NULL, will use that directory
+ * for persisting information. Will load any information persisted in earlier session.
+ * url needs only be specified for instances where this has never been persisted before.
+ *
+ * @param pacme   will hold the ACME server instance on success
+ * @param p       pool to used
+ * @param url     url of the server, optional if known at path
+ * @param proxy_url optional url of a HTTP(S) proxy to use
+ */
+apr_status_t md_acme_create(md_acme_t **pacme, apr_pool_t *p, const char *url,
+                            const char *proxy_url);
+
+/**
+ * Contact the ACME server and retrieve its directory information.
+ * 
+ * @param acme    the ACME server to contact
+ */
+apr_status_t md_acme_setup(md_acme_t *acme);
+
+/**************************************************************************************************/
+/* account handling */
+
+#define MD_ACME_ACCT_STAGED     "staged"
+
+apr_status_t md_acme_acct_load(struct md_acme_acct_t **pacct, struct md_pkey_t **ppkey,
+                               struct md_store_t *store, md_store_group_t group, 
+                               const char *name, apr_pool_t *p);
+
+/** 
+ * Specify the account to use by name in local store. On success, the account
+ * the "current" one used by the acme instance.
+ */
+apr_status_t md_acme_use_acct(md_acme_t *acme, struct md_store_t *store, 
+                              apr_pool_t *p, const char *acct_id);
+
+apr_status_t md_acme_use_acct_staged(md_acme_t *acme, struct md_store_t *store, 
+                                     md_t *md, apr_pool_t *p);
+
+/**
+ * Get the local name of the account currently used by the acme instance.
+ * Will be NULL if no account has been setup successfully.
+ */
+const char *md_acme_get_acct_id(md_acme_t *acme);
+
+/**
+ * Agree to the given Terms-of-Service url for the current account.
+ */
+apr_status_t md_acme_agree(md_acme_t *acme, apr_pool_t *p, const char *tos);
+
+/**
+ * Confirm with the server that the current account agrees to the Terms-of-Service
+ * given in the agreement url.
+ * If the known agreement is equal to this, nothing is done.
+ * If it differs, the account is re-validated in the hope that the server
+ * announces the Tos URL it wants. If this is equal to the agreement specified,
+ * the server is notified of this. If the server requires a ToS that the account
+ * thinks it has already given, it is resend.
+ *
+ * If an agreement is required, different from the current one, APR_INCOMPLETE is
+ * returned and the agreement url is returned in the parameter.
+ */
+apr_status_t md_acme_check_agreement(md_acme_t *acme, apr_pool_t *p, 
+                                     const char *agreement, const char **prequired);
+
+/**
+ * Get the ToS agreement for current account.
+ */
+const char *md_acme_get_agreement(md_acme_t *acme);
+
+
+/** 
+ * Find an existing account in the local store. On APR_SUCCESS, the acme
+ * instance will have a current, validated account to use.
+ */ 
+apr_status_t md_acme_find_acct(md_acme_t *acme, struct md_store_t *store, apr_pool_t *p);
+
+/**
+ * Create a new account at the ACME server. The
+ * new account is the one used by the acme instance afterwards, on success.
+ */
+apr_status_t md_acme_create_acct(md_acme_t *acme, apr_pool_t *p, apr_array_header_t *contacts, 
+                                 const char *agreement);
+
+apr_status_t md_acme_acct_save(struct md_store_t *store, apr_pool_t *p, md_acme_t *acme,  
+                               struct md_acme_acct_t *acct, struct md_pkey_t *acct_key);
+                               
+apr_status_t md_acme_save(md_acme_t *acme, struct md_store_t *store, apr_pool_t *p);
+
+apr_status_t md_acme_acct_save_staged(md_acme_t *acme, struct md_store_t *store, 
+                                      md_t *md, apr_pool_t *p);
+
+/**
+ * Delete the current account at the ACME server and remove it from store. 
+ */
+apr_status_t md_acme_delete_acct(md_acme_t *acme, struct md_store_t *store, apr_pool_t *p);
+
+/**
+ * Delete the account from the local store without contacting the ACME server.
+ */
+apr_status_t md_acme_unstore_acct(struct md_store_t *store, apr_pool_t *p, const char *acct_id);
+
+/**************************************************************************************************/
+/* request handling */
+
+/**
+ * Request callback on a successful HTTP response (status 2xx).
+ */
+typedef apr_status_t md_acme_req_res_cb(md_acme_t *acme, 
+                                        const struct md_http_response_t *res, void *baton);
+
+/**
+ * A request against an ACME server
+ */
+typedef struct md_acme_req_t md_acme_req_t;
+
+/**
+ * Request callback to initialize before sending. May be invoked more than once in
+ * case of retries.
+ */
+typedef apr_status_t md_acme_req_init_cb(md_acme_req_t *req, void *baton);
+
+/**
+ * Request callback on a successful response (HTTP response code 2xx) and content
+ * type matching application/.*json.
+ */
+typedef apr_status_t md_acme_req_json_cb(md_acme_t *acme, apr_pool_t *p, 
+                                         const apr_table_t *headers, 
+                                         struct md_json_t *jbody, void *baton);
+
+struct md_acme_req_t {
+    md_acme_t *acme;               /* the ACME server to talk to */
+    apr_pool_t *p;                 /* pool for the request duration */
+    
+    const char *url;               /* url to POST the request to */
+    const char *method;            /* HTTP method to use */
+    apr_table_t *prot_hdrs;        /* JWS headers needing protection (nonce) */
+    struct md_json_t *req_json;    /* JSON to be POSTed in request body */
+
+    apr_table_t *resp_hdrs;        /* HTTP response headers */
+    struct md_json_t *resp_json;   /* JSON response body received */
+    
+    apr_status_t rv;               /* status of request */
+    
+    md_acme_req_init_cb *on_init;  /* callback to initialize the request before submit */
+    md_acme_req_json_cb *on_json;  /* callback on successful JSON response */
+    md_acme_req_res_cb *on_res;    /* callback on generic HTTP response */
+    int max_retries;               /* how often this might be retried */
+    void *baton;                   /* userdata for callbacks */
+};
+
+apr_status_t md_acme_GET(md_acme_t *acme, const char *url,
+                         md_acme_req_init_cb *on_init,
+                         md_acme_req_json_cb *on_json,
+                         md_acme_req_res_cb *on_res,
+                         void *baton);
+/**
+ * Perform a POST against the ACME url. If a on_json callback is given and
+ * the HTTP response is JSON, only this callback is invoked. Otherwise, on HTTP status
+ * 2xx, the on_res callback is invoked. If no on_res is given, it is considered a
+ * response error, since only JSON was expected.
+ * At least one callback needs to be non-NULL.
+ * 
+ * @param acme        the ACME server to talk to
+ * @param url         the url to send the request to
+ * @param on_init     callback to initialize the request data
+ * @param on_json     callback on successful JSON response
+ * @param on_res      callback on successful HTTP response
+ * @param baton       userdata for callbacks
+ */
+apr_status_t md_acme_POST(md_acme_t *acme, const char *url,
+                          md_acme_req_init_cb *on_init,
+                          md_acme_req_json_cb *on_json,
+                          md_acme_req_res_cb *on_res,
+                          void *baton);
+
+apr_status_t md_acme_GET(md_acme_t *acme, const char *url,
+                         md_acme_req_init_cb *on_init,
+                         md_acme_req_json_cb *on_json,
+                         md_acme_req_res_cb *on_res,
+                         void *baton);
+
+/**
+ * Retrieve a JSON resource from the ACME server 
+ */
+apr_status_t md_acme_get_json(struct md_json_t **pjson, md_acme_t *acme, 
+                              const char *url, apr_pool_t *p);
+
+
+apr_status_t md_acme_req_body_init(md_acme_req_t *req, struct md_json_t *jpayload);
+
+apr_status_t md_acme_protos_add(struct apr_hash_t *protos, apr_pool_t *p);
+
+#endif /* md_acme_h */
diff --git a/modules/md/md_acme_acct.c b/modules/md/md_acme_acct.c
new file mode 100644 (file)
index 0000000..6be9ded
--- /dev/null
@@ -0,0 +1,669 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+
+#include <apr_lib.h>
+#include <apr_file_info.h>
+#include <apr_file_io.h>
+#include <apr_fnmatch.h>
+#include <apr_hash.h>
+#include <apr_strings.h>
+#include <apr_tables.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_jws.h"
+#include "md_log.h"
+#include "md_store.h"
+#include "md_util.h"
+#include "md_version.h"
+
+#include "md_acme.h"
+#include "md_acme_acct.h"
+
+static apr_status_t acct_make(md_acme_acct_t **pacct, apr_pool_t *p, 
+                              const char *ca_url, const char *id, apr_array_header_t *contacts)
+{
+    md_acme_acct_t *acct;
+    
+    acct = apr_pcalloc(p, sizeof(*acct));
+
+    acct->id = id? apr_pstrdup(p, id) : NULL;
+    acct->ca_url = ca_url;
+    
+    if (!contacts || apr_is_empty_array(contacts)) {
+        acct->contacts = apr_array_make(p, 5, sizeof(const char *));
+    }
+    else {
+        acct->contacts = apr_array_copy(p, contacts);
+    }
+    
+    *pacct = acct;
+    return APR_SUCCESS;
+}
+
+
+static const char *mk_acct_id(apr_pool_t *p, md_acme_t *acme, int i)
+{
+    return apr_psprintf(p, "ACME-%s-%04d", acme->sname, i);
+}
+
+static const char *mk_acct_pattern(apr_pool_t *p, md_acme_t *acme)
+{
+    return apr_psprintf(p, "ACME-%s-*", acme->sname);
+}
+/**************************************************************************************************/
+/* json load/save */
+
+static md_json_t *acct_to_json(md_acme_acct_t *acct, apr_pool_t *p)
+{
+    md_json_t *jacct;
+
+    assert(acct);
+    jacct = md_json_create(p);
+    md_json_sets(acct->id, jacct, MD_KEY_ID, NULL);
+    md_json_setb(acct->disabled, jacct, MD_KEY_DISABLED, NULL);
+    md_json_sets(acct->url, jacct, MD_KEY_URL, NULL);
+    md_json_sets(acct->ca_url, jacct, MD_KEY_CA_URL, NULL);
+    md_json_setj(acct->registration, jacct, MD_KEY_REGISTRATION, NULL);
+    if (acct->agreement) {
+        md_json_sets(acct->agreement, jacct, MD_KEY_AGREEMENT, NULL);
+    }
+    
+    return jacct;
+}
+
+static apr_status_t acct_from_json(md_acme_acct_t **pacct, md_json_t *json, apr_pool_t *p)
+{
+    apr_status_t rv = APR_EINVAL;
+    md_acme_acct_t *acct;
+    int disabled;
+    const char *ca_url, *url, *id;
+    apr_array_header_t *contacts;
+    
+    id = md_json_gets(json, MD_KEY_ID, NULL);
+    disabled = md_json_getb(json, MD_KEY_DISABLED, NULL);
+    ca_url = md_json_gets(json, MD_KEY_CA_URL, NULL);
+    if (!ca_url) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "account has no CA url: %s", id);
+        goto out;
+    }
+    
+    url = md_json_gets(json, MD_KEY_URL, NULL);
+    if (!url) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "account has no url: %s", id);
+        goto out;
+    }
+
+    contacts = apr_array_make(p, 5, sizeof(const char *));
+    md_json_getsa(contacts, json, MD_KEY_REGISTRATION, MD_KEY_CONTACT, NULL);
+    rv = acct_make(&acct, p, ca_url, id, contacts);
+    if (APR_SUCCESS == rv) {
+        acct->disabled = disabled;
+        acct->url = url;
+        acct->agreement = md_json_gets(json, "terms-of-service", NULL);
+    }
+
+out:
+    *pacct = (APR_SUCCESS == rv)? acct : NULL;
+    return rv;
+}
+
+apr_status_t md_acme_acct_save_staged(md_acme_t *acme, md_store_t *store, md_t *md, apr_pool_t *p)
+{
+    md_acme_acct_t *acct = acme->acct;
+    md_json_t *jacct;
+    apr_status_t rv;
+    
+    jacct = acct_to_json(acct, p);
+    
+    rv = md_store_save(store, p, MD_SG_STAGING, md->name, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 0);
+    if (APR_SUCCESS == rv) {
+        rv = md_store_save(store, p, MD_SG_STAGING, md->name, MD_FN_ACCT_KEY, 
+                           MD_SV_PKEY, acme->acct_key, 0);
+    }
+    return rv;
+}
+
+apr_status_t md_acme_acct_save(md_store_t *store, apr_pool_t *p, md_acme_t *acme, 
+                               md_acme_acct_t *acct, md_pkey_t *acct_key)
+{
+    md_json_t *jacct;
+    apr_status_t rv;
+    int i;
+    const char *id;
+    
+    jacct = acct_to_json(acct, p);
+    id = acct->id;
+    
+    if (id) {
+        rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 0);
+    }
+    else {
+        rv = APR_EAGAIN;
+        for (i = 0; i < 1000 && APR_SUCCESS != rv; ++i) {
+            id = mk_acct_id(p, acme, i);
+            md_json_sets(id, jacct, MD_KEY_ID, NULL);
+            rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCOUNT, MD_SV_JSON, jacct, 1);
+        }
+        
+    }
+    if (APR_SUCCESS == rv) {
+        acct->id = id;
+        rv = md_store_save(store, p, MD_SG_ACCOUNTS, id, MD_FN_ACCT_KEY, MD_SV_PKEY, acct_key, 0);
+    }
+    return rv;
+}
+
+apr_status_t md_acme_save(md_acme_t *acme, md_store_t *store, apr_pool_t *p)
+{
+    return md_acme_acct_save(store, p, acme, acme->acct, acme->acct_key); 
+}
+
+apr_status_t md_acme_acct_load(md_acme_acct_t **pacct, md_pkey_t **ppkey,
+                               md_store_t *store, md_store_group_t group, 
+                               const char *name, apr_pool_t *p)
+{
+    md_json_t *json;
+    apr_status_t rv;
+
+    rv = md_store_load_json(store, group, name, MD_FN_ACCOUNT, &json, p);
+    if (APR_STATUS_IS_ENOENT(rv)) {
+        goto out;
+    }
+    if (APR_SUCCESS != rv) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "error reading account: %s", name);
+        goto out;
+    }
+    
+    rv = acct_from_json(pacct, json, p);
+    if (APR_SUCCESS == rv) {
+        rv = md_store_load(store, group, name, MD_FN_ACCT_KEY, MD_SV_PKEY, (void**)ppkey, p);
+        if (APR_SUCCESS != rv) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "loading key: %s", name);
+            goto out;
+        }
+    }
+out:
+    if (APR_SUCCESS != rv) {
+        *pacct = NULL;
+        *ppkey = NULL;
+    } 
+    return rv;
+}
+
+/**************************************************************************************************/
+/* Lookup */
+
+typedef struct {
+    apr_pool_t *p;
+    md_acme_t *acme;
+    const char *id;
+} find_ctx;
+
+static int find_acct(void *baton, const char *name, const char *aspect,
+                     md_store_vtype_t vtype, void *value, apr_pool_t *ptemp)
+{
+    find_ctx *ctx = baton;
+    int disabled;
+    const char *ca_url, *id;
+    
+    (void)aspect;
+    (void)ptemp;
+    if (MD_SV_JSON == vtype) {
+        md_json_t *json = value;
+        
+        id = md_json_gets(json, MD_KEY_ID, NULL);
+        disabled = md_json_getb(json, MD_KEY_DISABLED, NULL);
+        ca_url = md_json_gets(json, MD_KEY_CA_URL, NULL);
+        
+        if (!disabled && ca_url && !strcmp(ctx->acme->url, ca_url)) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ctx->p, 
+                          "found account %s for %s: %s, disabled=%d, ca-url=%s", 
+                          name, ctx->acme->url, id, disabled, ca_url);
+            ctx->id = id;
+            return 0;
+        }
+    }
+    return 1;
+}
+
+static apr_status_t acct_find(md_acme_acct_t **pacct, md_pkey_t **ppkey, 
+                              md_store_t *store, md_acme_t *acme, apr_pool_t *p)
+{
+    apr_status_t rv;
+    find_ctx ctx;
+    
+    ctx.p = p;
+    ctx.acme = acme;
+    ctx.id = NULL;
+    
+    rv = md_store_iter(find_acct, &ctx, store, p, MD_SG_ACCOUNTS, mk_acct_pattern(p, acme),
+                       MD_FN_ACCOUNT, MD_SV_JSON);
+    if (ctx.id) {
+        rv = md_acme_acct_load(pacct, ppkey, store, MD_SG_ACCOUNTS, ctx.id, p);
+    }
+    else {
+        *pacct = NULL;
+        rv = APR_ENOENT;
+    }
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, 
+                  "acct_find %s", (*pacct)? (*pacct)->id : "NULL"); 
+    return rv;
+}
+
+/**************************************************************************************************/
+/* Register a new account */
+
+typedef struct {
+    md_acme_t *acme;
+    apr_pool_t *p;
+} acct_ctx_t;
+
+static apr_status_t on_init_acct_new(md_acme_req_t *req, void *baton)
+{
+    acct_ctx_t *ctx = baton;
+    md_json_t *jpayload;
+
+    jpayload = md_json_create(req->p);
+    md_json_sets("new-reg", jpayload, MD_KEY_RESOURCE, NULL);
+    md_json_setsa(ctx->acme->acct->contacts, jpayload, MD_KEY_CONTACT, NULL);
+    if (ctx->acme->acct->agreement) {
+        md_json_sets(ctx->acme->acct->agreement, jpayload, MD_KEY_AGREEMENT, NULL);
+    }
+    
+    return md_acme_req_body_init(req, jpayload);
+} 
+
+static apr_status_t acct_upd(md_acme_t *acme, apr_pool_t *p, 
+                             const apr_table_t *hdrs, md_json_t *body, void *baton)
+{
+    acct_ctx_t *ctx = baton;
+    apr_status_t rv = APR_SUCCESS;
+    md_acme_acct_t *acct = acme->acct;
+    
+    if (!acct->url) {
+        const char *location = apr_table_get(hdrs, "location");
+        if (!location) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, APR_EINVAL, p, "new acct without location");
+            return APR_EINVAL;
+        }
+        acct->url = apr_pstrdup(ctx->p, location);
+    }
+    if (!acct->tos_required) {
+        acct->tos_required = md_link_find_relation(hdrs, ctx->p, "terms-of-service");
+        if (acct->tos_required) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, 
+                          "server requires agreement to <%s>", acct->tos_required);
+        }
+    }
+    
+    apr_array_clear(acct->contacts);
+    md_json_getsa(acct->contacts, body, MD_KEY_CONTACT, NULL);
+    acct->registration = md_json_clone(ctx->p, body);
+    
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "updated acct %s", acct->url);
+    return rv;
+}
+
+static apr_status_t acct_register(md_acme_t *acme, apr_pool_t *p,  
+                                  apr_array_header_t *contacts, const char *agreement)
+{
+    apr_status_t rv;
+    md_pkey_t *pkey;
+    const char *err = NULL, *uri;
+    md_pkey_spec_t spec;
+    int i;
+    
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "create new account");
+    
+    if (agreement) {
+        if (APR_SUCCESS != (rv = md_util_abs_uri_check(acme->p, agreement, &err))) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, 
+                          "invalid agreement uri (%s): %s", err, agreement);
+            goto out;
+        }
+    }
+    for (i = 0; i < contacts->nelts; ++i) {
+        uri = APR_ARRAY_IDX(contacts, i, const char *);
+        if (APR_SUCCESS != (rv = md_util_abs_uri_check(acme->p, uri, &err))) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, 
+                          "invalid contact uri (%s): %s", err, uri);
+            goto out;
+        }
+    }
+    
+    spec.type = MD_PKEY_TYPE_RSA;
+    spec.params.rsa.bits = MD_ACME_ACCT_PKEY_BITS;
+    
+    if (APR_SUCCESS == (rv = md_pkey_gen(&pkey, acme->p, &spec))
+        && APR_SUCCESS == (rv = acct_make(&acme->acct,  p, acme->url, NULL, contacts))) {
+        acct_ctx_t ctx;
+
+        acme->acct_key = pkey;
+        if (agreement) {
+            acme->acct->agreement = agreement;
+        }
+
+        ctx.acme = acme;
+        ctx.p = p;
+        rv = md_acme_POST(acme, acme->new_reg, on_init_acct_new, acct_upd, NULL, &ctx);
+        if (APR_SUCCESS == rv) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, p, 
+                          "registered new account %s", acme->acct->url);
+        }
+    }
+
+out:    
+    if (APR_SUCCESS != rv && acme->acct) {
+        acme->acct = NULL;
+    }
+    return rv;
+}
+
+/**************************************************************************************************/
+/* acct validation */
+
+static apr_status_t on_init_acct_valid(md_acme_req_t *req, void *baton)
+{
+    md_json_t *jpayload;
+
+    (void)baton;
+    jpayload = md_json_create(req->p);
+    md_json_sets("reg", jpayload, MD_KEY_RESOURCE, NULL);
+    
+    return md_acme_req_body_init(req, jpayload);
+} 
+
+static apr_status_t acct_valid(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs, 
+                               md_json_t *body, void *baton)
+{
+    md_acme_acct_t *acct = acme->acct;
+    apr_status_t rv = APR_SUCCESS;
+    const char *body_str;
+    const char *tos_required;
+    
+    (void)p;
+    (void)baton;
+    apr_array_clear(acct->contacts);
+    md_json_getsa(acct->contacts, body, MD_KEY_CONTACT, NULL);
+    acct->registration = md_json_clone(acme->p, body);
+    
+    body_str = md_json_writep(body, acme->p, MD_JSON_FMT_INDENT);
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, acme->p, "validate acct %s: %s", 
+                  acct->url, body_str ? body_str : "<failed to serialize!>");
+    
+    acct->agreement = md_json_gets(acct->registration, MD_KEY_AGREEMENT, NULL);
+    tos_required = md_link_find_relation(hdrs, acme->p, "terms-of-service");
+    
+    if (tos_required) {
+        if (!acct->agreement || strcmp(tos_required, acct->agreement)) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, acme->p, 
+                          "needs to agree to terms-of-service '%s', "
+                          "has already agreed to '%s'", 
+                          tos_required, acct->agreement);
+        }
+        acct->tos_required = tos_required;
+    }
+    
+    return rv;
+}
+
+static apr_status_t md_acme_validate_acct(md_acme_t *acme)
+{
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "acct validation");
+    if (!acme->acct) {
+        return APR_EINVAL;
+    }
+    return md_acme_POST(acme, acme->acct->url, on_init_acct_valid, acct_valid, NULL, NULL);
+}
+
+/**************************************************************************************************/
+/* account setup */
+
+static apr_status_t acct_validate(md_acme_t *acme, md_store_t *store, apr_pool_t *p)
+{
+    apr_status_t rv;
+    
+    if (APR_SUCCESS != (rv = md_acme_validate_acct(acme))) {
+        if (acme->acct && (APR_ENOENT == rv || APR_EACCES == rv)) {
+            if (!acme->acct->disabled) {
+                acme->acct->disabled = 1;
+                if (store) {
+                    md_acme_save(acme, store, p);
+                }
+            }
+            acme->acct = NULL;
+            acme->acct_key = NULL;
+            rv = APR_ENOENT;
+        }
+    }
+    return rv;
+}
+
+apr_status_t md_acme_use_acct(md_acme_t *acme, md_store_t *store,
+                              apr_pool_t *p, const char *acct_id)
+{
+    md_acme_acct_t *acct;
+    md_pkey_t *pkey;
+    apr_status_t rv;
+    
+    if (APR_SUCCESS == (rv = md_acme_acct_load(&acct, &pkey, 
+                                               store, MD_SG_ACCOUNTS, acct_id, acme->p))) {
+        if (acct->ca_url && !strcmp(acct->ca_url, acme->url)) {
+            acme->acct = acct;
+            acme->acct_key = pkey;
+            rv = acct_validate(acme, store, p);
+        }
+        else {
+            /* account is from a nother server or, more likely, from another
+             * protocol endpoint on the same server */
+            rv = APR_ENOENT;
+        }
+    }
+    return rv;
+}
+
+apr_status_t md_acme_use_acct_staged(md_acme_t *acme, struct md_store_t *store, 
+                                     md_t *md, apr_pool_t *p)
+{
+    md_acme_acct_t *acct;
+    md_pkey_t *pkey;
+    apr_status_t rv;
+    
+    if (APR_SUCCESS == (rv = md_acme_acct_load(&acct, &pkey, 
+                                               store, MD_SG_STAGING, md->name, acme->p))) {
+        acme->acct = acct;
+        acme->acct_key = pkey;
+        rv = acct_validate(acme, NULL, p);
+    }
+    return rv;
+}
+
+const char *md_acme_get_acct_id(md_acme_t *acme)
+{
+    return acme->acct? acme->acct->id : NULL;
+}
+
+const char *md_acme_get_agreement(md_acme_t *acme)
+{
+    return acme->acct? acme->acct->agreement : NULL;
+}
+
+apr_status_t md_acme_find_acct(md_acme_t *acme, md_store_t *store, apr_pool_t *p)
+{
+    md_acme_acct_t *acct;
+    md_pkey_t *pkey;
+    apr_status_t rv;
+    
+    while (APR_SUCCESS == acct_find(&acct, &pkey, store, acme, acme->p)) {
+        acme->acct = acct;
+        acme->acct_key = pkey;
+        rv = acct_validate(acme, store, p);
+        
+        if (APR_SUCCESS == rv) {
+            return rv;
+        }
+        else {
+            acme->acct = NULL;
+            acme->acct_key = NULL;
+            if (!APR_STATUS_IS_ENOENT(rv)) {
+                /* encountered error with server */
+                return rv;
+            }
+        }
+    }
+    return APR_ENOENT;
+}
+
+apr_status_t md_acme_create_acct(md_acme_t *acme, apr_pool_t *p, apr_array_header_t *contacts, 
+                                 const char *agreement)
+{
+    return acct_register(acme, p, contacts, agreement);
+}
+
+/**************************************************************************************************/
+/* Delete the account */
+
+apr_status_t md_acme_unstore_acct(md_store_t *store, apr_pool_t *p, const char *acct_id) 
+{
+    apr_status_t rv = APR_SUCCESS;
+    
+    rv = md_store_remove(store, MD_SG_ACCOUNTS, acct_id, MD_FN_ACCOUNT, p, 1);
+    if (APR_SUCCESS == rv) {
+        md_store_remove(store, MD_SG_ACCOUNTS, acct_id, MD_FN_ACCT_KEY, p, 1);
+    }
+    return rv;
+}
+
+static apr_status_t on_init_acct_del(md_acme_req_t *req, void *baton)
+{
+    md_json_t *jpayload;
+
+    (void)baton;
+    jpayload = md_json_create(req->p);
+    md_json_sets("reg", jpayload, MD_KEY_RESOURCE, NULL);
+    md_json_setb(1, jpayload, "delete", NULL);
+    
+    return md_acme_req_body_init(req, jpayload);
+} 
+
+static apr_status_t acct_del(md_acme_t *acme, apr_pool_t *p,
+                             const apr_table_t *hdrs, md_json_t *body, void *baton)
+{
+    md_store_t *store = baton;
+    apr_status_t rv = APR_SUCCESS;
+
+    (void)hdrs;
+    (void)body;
+    md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, p, "deleted account %s", acme->acct->url);
+    if (store) {
+        rv = md_acme_unstore_acct(store, p, acme->acct->id);
+        acme->acct = NULL;
+        acme->acct_key = NULL;
+    }
+    return rv;
+}
+
+apr_status_t md_acme_delete_acct(md_acme_t *acme, md_store_t *store, apr_pool_t *p)
+{
+    md_acme_acct_t *acct = acme->acct;
+    
+    (void)p;
+    if (!acct) {
+        return APR_EINVAL;
+    }
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "delete account %s from %s", 
+                  acct->url, acct->ca_url);
+    return md_acme_POST(acme, acct->url, on_init_acct_del, acct_del, NULL, store);
+}
+
+/**************************************************************************************************/
+/* terms-of-service */
+
+static apr_status_t on_init_agree_tos(md_acme_req_t *req, void *baton)
+{
+    acct_ctx_t *ctx = baton;
+    md_json_t *jpayload;
+
+    jpayload = md_json_create(req->p);
+    md_json_sets("reg", jpayload, MD_KEY_RESOURCE, NULL);
+    md_json_sets(ctx->acme->acct->agreement, jpayload, MD_KEY_AGREEMENT, NULL);
+    
+    return md_acme_req_body_init(req, jpayload);
+} 
+
+apr_status_t md_acme_agree(md_acme_t *acme, apr_pool_t *p, const char *agreement)
+{
+    acct_ctx_t ctx;
+    
+    acme->acct->agreement = agreement;
+    ctx.acme = acme;
+    ctx.p = p;
+    return md_acme_POST(acme, acme->acct->url, on_init_agree_tos, acct_upd, NULL, &ctx);
+}
+
+static int agreement_required(md_acme_acct_t *acct)
+{
+    /* We used to really check if the account agreement and the one
+     * indicated as valid are the very same:
+     * return (!acct->agreement 
+     *       || (acct->tos_required && strcmp(acct->tos_required, acct->agreement)));
+     * However, LE is happy if the account has agreed to a ToS in the past and
+     * does not required a renewed acceptance.
+     */
+     return !acct->agreement; 
+}
+
+apr_status_t md_acme_check_agreement(md_acme_t *acme, apr_pool_t *p, 
+                                     const char *agreement, const char **prequired)
+{
+    apr_status_t rv = APR_SUCCESS;
+    
+    /* Check if (correct) Terms-of-Service for account were accepted */
+    *prequired = NULL;
+    if (agreement_required(acme->acct)) {
+        const char *tos = acme->acct->tos_required;
+        if (!tos) {
+            if (APR_SUCCESS != (rv = md_acme_validate_acct(acme))) {
+                md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, acme->p, 
+                              "validate for account %", acme->acct->id); 
+                return rv;
+            }
+            tos = acme->acct->tos_required; 
+            if (!tos) {
+                md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, acme->p, "unknown terms-of-service "
+                              "required after validation of account %", acme->acct->id); 
+                return APR_EGENERAL;
+            }
+        }
+        
+        if (acme->acct->agreement && !strcmp(tos, acme->acct->agreement)) {
+            rv = md_acme_agree(acme, p, tos);
+        }
+        else if (agreement && !strcmp(tos, agreement)) {
+            rv = md_acme_agree(acme, p, tos);
+        }
+        else {
+            *prequired = apr_pstrdup(p, tos);
+            rv = APR_INCOMPLETE;
+        }
+    }
+    return rv;
+}        
diff --git a/modules/md/md_acme_acct.h b/modules/md/md_acme_acct.h
new file mode 100644 (file)
index 0000000..06ac953
--- /dev/null
@@ -0,0 +1,48 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef mod_md_md_acme_acct_h
+#define mod_md_md_acme_acct_h
+
+struct md_acme_req;
+struct md_json_t;
+struct md_pkey_t;
+
+
+/** 
+ * An ACME account at an ACME server.
+ */
+typedef struct md_acme_acct_t md_acme_acct_t;
+
+struct md_acme_acct_t {
+    const char *id;                 /* short, unique id for the account */
+    const char *url;                /* url of the account, once registered */
+    const char *ca_url;             /* url of the ACME protocol endpoint */
+    apr_array_header_t *contacts;   /* list of contact uris, e.g. mailto:xxx */
+    const char *tos_required;       /* terms of service asked for by CA */
+    const char *agreement;          /* terms of service agreed to by user */
+    
+    struct md_json_t *registration; /* data from server registration */
+    int disabled;
+};
+
+#define MD_FN_ACCOUNT           "account.json"
+#define MD_FN_ACCT_KEY          "account.pem"
+
+/* ACME account private keys are always RSA and have that many bits. Since accounts
+ * are expected to live long, better err on the safe side. */
+#define MD_ACME_ACCT_PKEY_BITS  3072
+
+#endif /* md_acme_acct_h */
diff --git a/modules/md/md_acme_authz.c b/modules/md/md_acme_authz.c
new file mode 100644 (file)
index 0000000..2fb893c
--- /dev/null
@@ -0,0 +1,708 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+
+#include <apr_lib.h>
+#include <apr_buckets.h>
+#include <apr_file_info.h>
+#include <apr_file_io.h>
+#include <apr_fnmatch.h>
+#include <apr_hash.h>
+#include <apr_strings.h>
+#include <apr_tables.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_http.h"
+#include "md_log.h"
+#include "md_jws.h"
+#include "md_store.h"
+#include "md_util.h"
+
+#include "md_acme.h"
+#include "md_acme_authz.h"
+
+md_acme_authz_t *md_acme_authz_create(apr_pool_t *p)
+{
+    md_acme_authz_t *authz;
+    authz = apr_pcalloc(p, sizeof(*authz));
+    
+    return authz;
+}
+
+md_acme_authz_set_t *md_acme_authz_set_create(apr_pool_t *p)
+{
+    md_acme_authz_set_t *authz_set;
+    
+    authz_set = apr_pcalloc(p, sizeof(*authz_set));
+    authz_set->authzs = apr_array_make(p, 5, sizeof(md_acme_authz_t *));
+    
+    return authz_set;
+}
+
+md_acme_authz_t *md_acme_authz_set_get(md_acme_authz_set_t *set, const char *domain)
+{
+    md_acme_authz_t *authz;
+    int i;
+    
+    assert(domain);
+    for (i = 0; i < set->authzs->nelts; ++i) {
+        authz = APR_ARRAY_IDX(set->authzs, i, md_acme_authz_t *);
+        if (!apr_strnatcasecmp(domain, authz->domain)) {
+            return authz;
+        }
+    }
+    return NULL;
+}
+
+apr_status_t md_acme_authz_set_add(md_acme_authz_set_t *set, md_acme_authz_t *authz)
+{
+    md_acme_authz_t *existing;
+    
+    assert(authz->domain);
+    if (NULL != (existing = md_acme_authz_set_get(set, authz->domain))) {
+        return APR_EINVAL;
+    }
+    APR_ARRAY_PUSH(set->authzs, md_acme_authz_t*) = authz;
+    return APR_SUCCESS;
+}
+
+apr_status_t md_acme_authz_set_remove(md_acme_authz_set_t *set, const char *domain)
+{
+    md_acme_authz_t *authz;
+    int i;
+    
+    assert(domain);
+    for (i = 0; i < set->authzs->nelts; ++i) {
+        authz = APR_ARRAY_IDX(set->authzs, i, md_acme_authz_t *);
+        if (!apr_strnatcasecmp(domain, authz->domain)) {
+            int n = i + 1;
+            if (n < set->authzs->nelts) {
+                void **elems = (void **)set->authzs->elts;
+                memmove(elems + i, elems + n, (size_t)(set->authzs->nelts - n)); 
+            }
+            --set->authzs->nelts;
+            return APR_SUCCESS;
+        }
+    }
+    return APR_ENOENT;
+}
+
+/**************************************************************************************************/
+/* Register a new authorization */
+
+typedef struct {
+    size_t index;
+    const char *type;
+    const char *uri;
+    const char *token;
+    const char *key_authz;
+} md_acme_authz_cha_t;
+
+typedef struct {
+    apr_pool_t *p;
+    md_acme_t *acme;
+    const char *domain;
+    md_acme_authz_t *authz;
+    md_acme_authz_cha_t *challenge;
+} authz_req_ctx;
+
+static void authz_req_ctx_init(authz_req_ctx *ctx, md_acme_t *acme, 
+                               const char *domain, md_acme_authz_t *authz, apr_pool_t *p)
+{
+    memset(ctx, 0, sizeof(*ctx));
+    ctx->p = p;
+    ctx->acme = acme;
+    ctx->domain = domain;
+    ctx->authz = authz;
+}
+
+static apr_status_t on_init_authz(md_acme_req_t *req, void *baton)
+{
+    authz_req_ctx *ctx = baton;
+    md_json_t *jpayload;
+
+    jpayload = md_json_create(req->p);
+    md_json_sets("new-authz", jpayload, MD_KEY_RESOURCE, NULL);
+    md_json_sets("dns", jpayload, MD_KEY_IDENTIFIER, MD_KEY_TYPE, NULL);
+    md_json_sets(ctx->domain, jpayload, MD_KEY_IDENTIFIER, MD_KEY_VALUE, NULL);
+    
+    return md_acme_req_body_init(req, jpayload);
+} 
+
+static apr_status_t authz_created(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs, 
+                                  md_json_t *body, void *baton)
+{
+    authz_req_ctx *ctx = baton;
+    const char *location = apr_table_get(hdrs, "location");
+    apr_status_t rv = APR_SUCCESS;
+    
+    (void)acme;
+    (void)p;
+    if (location) {
+        ctx->authz = md_acme_authz_create(ctx->p);
+        ctx->authz->domain = apr_pstrdup(ctx->p, ctx->domain);
+        ctx->authz->location = apr_pstrdup(ctx->p, location);
+        ctx->authz->resource = md_json_clone(ctx->p, body);
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, ctx->p, "authz_new at %s", location);
+    }
+    else {
+        rv = APR_EINVAL;
+        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, ctx->p, "new authz, no location header");
+    }
+    return rv;
+}
+
+apr_status_t md_acme_authz_register(struct md_acme_authz_t **pauthz, md_acme_t *acme, 
+                                    md_store_t *store, const char *domain, apr_pool_t *p)
+{
+    apr_status_t rv;
+    authz_req_ctx ctx;
+    
+    (void)store;
+    authz_req_ctx_init(&ctx, acme, domain, NULL, p);
+    
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, acme->p, "create new authz");
+    rv = md_acme_POST(acme, acme->new_authz, on_init_authz, authz_created, NULL, &ctx);
+    
+    *pauthz = (APR_SUCCESS == rv)? ctx.authz : NULL;
+    return rv;
+}
+
+/**************************************************************************************************/
+/* Update an existing authorization */
+
+apr_status_t md_acme_authz_update(md_acme_authz_t *authz, md_acme_t *acme, 
+                                  md_store_t *store, apr_pool_t *p)
+{
+    md_json_t *json;
+    const char *s;
+    apr_status_t rv;
+    
+    (void)store;
+    assert(acme);
+    assert(acme->http);
+    assert(authz);
+    assert(authz->location);
+
+    if (APR_SUCCESS != (rv = md_acme_get_json(&json, acme, authz->location, p))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "update authz for %s at %s",
+                      authz->domain, authz->location);
+        return rv;
+    }
+    
+    authz->resource = json;
+    s = md_json_gets(json, "identifier", "type", NULL);
+    if (!s || strcmp(s, "dns")) return APR_EINVAL;
+    s = md_json_gets(json, "identifier", "value", NULL);
+    if (!s || strcmp(s, authz->domain)) return APR_EINVAL;
+    
+    authz->state = MD_ACME_AUTHZ_S_UNKNOWN;
+    s = md_json_gets(json, "status", NULL);
+    if (s && !strcmp(s, "pending")) {
+        authz->state = MD_ACME_AUTHZ_S_PENDING;
+    }
+    else if (s && !strcmp(s, "valid")) {
+        authz->state = MD_ACME_AUTHZ_S_VALID;
+    }
+    else if (s && !strcmp(s, "invalid")) {
+        authz->state = MD_ACME_AUTHZ_S_INVALID;
+    }
+    else if (s) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, "unknown authz state '%s' "
+                      "for %s in %s", s, authz->domain, authz->location);
+        return APR_EINVAL;
+    }
+    return rv;
+}
+
+/**************************************************************************************************/
+/* response to a challenge */
+
+static md_acme_authz_cha_t *cha_from_json(apr_pool_t *p, size_t index, md_json_t *json)
+{
+    md_acme_authz_cha_t * cha;
+    
+    cha = apr_pcalloc(p, sizeof(*cha));
+    cha->index = index;
+    cha->type = md_json_dups(p, json, MD_KEY_TYPE, NULL);
+    cha->uri = md_json_dups(p, json, MD_KEY_URI, NULL);
+    cha->token = md_json_dups(p, json, MD_KEY_TOKEN, NULL);
+    cha->key_authz = md_json_dups(p, json, MD_KEY_KEYAUTHZ, NULL);
+
+    return cha;
+}
+
+static apr_status_t on_init_authz_resp(md_acme_req_t *req, void *baton)
+{
+    authz_req_ctx *ctx = baton;
+    md_json_t *jpayload;
+
+    jpayload = md_json_create(req->p);
+    md_json_sets("challenge", jpayload, MD_KEY_RESOURCE, NULL);
+    md_json_sets(ctx->challenge->key_authz, jpayload, MD_KEY_KEYAUTHZ, NULL);
+    
+    return md_acme_req_body_init(req, jpayload);
+} 
+
+static apr_status_t authz_http_set(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs, 
+                                   md_json_t *body, void *baton)
+{
+    authz_req_ctx *ctx = baton;
+    
+    (void)acme;
+    (void)p;
+    (void)hdrs;
+    (void)body;
+    md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, ctx->p, "updated authz %s", ctx->authz->location);
+    return APR_SUCCESS;
+}
+
+static apr_status_t setup_key_authz(md_acme_authz_cha_t *cha, md_acme_authz_t *authz,
+                                    md_acme_t *acme, apr_pool_t *p, int *pchanged)
+{
+    const char *thumb64, *key_authz;
+    apr_status_t rv;
+    
+    (void)authz;
+    assert(cha);
+    assert(cha->token);
+    
+    *pchanged = 0;
+    if (APR_SUCCESS == (rv = md_jws_pkey_thumb(&thumb64, p, acme->acct_key))) {
+        key_authz = apr_psprintf(p, "%s.%s", cha->token, thumb64);
+        if (cha->key_authz) {
+            if (strcmp(key_authz, cha->key_authz)) {
+                /* Hu? Did the account change key? */
+                cha->key_authz = NULL;
+            }
+        }
+        if (!cha->key_authz) {
+            cha->key_authz = key_authz;
+            *pchanged = 1;
+        }
+    }
+    return rv;
+}
+
+static apr_status_t cha_http_01_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, 
+                                      md_acme_t *acme, md_store_t *store, 
+                                      md_pkey_spec_t *key_spec, apr_pool_t *p)
+{
+    const char *data;
+    apr_status_t rv;
+    int notify_server;
+    
+    (void)key_spec;
+    if (APR_SUCCESS != (rv = setup_key_authz(cha, authz, acme, p, &notify_server))) {
+        goto out;
+    }
+    
+    rv = md_store_load(store, MD_SG_CHALLENGES, authz->domain, MD_FN_HTTP01,
+                       MD_SV_TEXT, (void**)&data, p);
+    if ((APR_SUCCESS == rv && strcmp(cha->key_authz, data)) || APR_STATUS_IS_ENOENT(rv)) {
+        rv = md_store_save(store, p, MD_SG_CHALLENGES, authz->domain, MD_FN_HTTP01,
+                           MD_SV_TEXT, (void*)cha->key_authz, 0);
+        authz->dir = authz->domain;
+        notify_server = 1;
+    }
+    
+    if (APR_SUCCESS == rv && notify_server) {
+        authz_req_ctx ctx;
+
+        /* challenge is setup or was changed from previous data, tell ACME server
+         * so it may (re)try verification */        
+        authz_req_ctx_init(&ctx, acme, NULL, authz, p);
+        ctx.challenge = cha;
+        rv = md_acme_POST(acme, cha->uri, on_init_authz_resp, authz_http_set, NULL, &ctx);
+    }
+out:
+    return rv;
+}
+
+static apr_status_t setup_cha_dns(const char **pdns, md_acme_authz_cha_t *cha, apr_pool_t *p)
+{
+    const char *dhex;
+    char *dns;
+    apr_size_t dhex_len;
+    apr_status_t rv;
+    
+    rv = md_crypt_sha256_digest_hex(&dhex, p, cha->key_authz, strlen(cha->key_authz));
+    if (APR_SUCCESS == rv) {
+        dhex = md_util_str_tolower((char*)dhex);
+        dhex_len = strlen(dhex); 
+        assert(dhex_len > 32);
+        dns = apr_pcalloc(p, dhex_len + 1 + sizeof(MD_TLSSNI01_DNS_SUFFIX));
+        strncpy(dns, dhex, 32);
+        dns[32] = '.';
+        strncpy(dns+33, dhex+32, dhex_len-32);
+        memcpy(dns+(dhex_len+1), MD_TLSSNI01_DNS_SUFFIX, sizeof(MD_TLSSNI01_DNS_SUFFIX));
+    }
+    *pdns = (APR_SUCCESS == rv)? dns : NULL;
+    return rv;
+}
+
+static apr_status_t cha_tls_sni_01_setup(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, 
+                                         md_acme_t *acme, md_store_t *store, 
+                                         md_pkey_spec_t *key_spec, apr_pool_t *p)
+{
+    md_cert_t *cha_cert;
+    md_pkey_t *cha_key;
+    const char *cha_dns;
+    apr_status_t rv;
+    int notify_server;
+    apr_array_header_t *domains;
+    
+    if (   APR_SUCCESS != (rv = setup_key_authz(cha, authz, acme, p, &notify_server))
+        || APR_SUCCESS != (rv = setup_cha_dns(&cha_dns, cha, p))) {
+        goto out;
+    }
+
+    rv = md_store_load(store, MD_SG_CHALLENGES, cha_dns, MD_FN_TLSSNI01_CERT,
+                       MD_SV_CERT, (void**)&cha_cert, p);
+    if ((APR_SUCCESS == rv && !md_cert_covers_domain(cha_cert, cha_dns)) 
+        || APR_STATUS_IS_ENOENT(rv)) {
+        
+        if (APR_SUCCESS != (rv = md_pkey_gen(&cha_key, p, key_spec))) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: create tls-sni-01 challenge key",
+                          authz->domain);
+            goto out;
+        }
+
+        /* setup a certificate containing the challenge dns */
+        domains = apr_array_make(p, 5, sizeof(const char*));
+        APR_ARRAY_PUSH(domains, const char*) = cha_dns;
+        rv = md_cert_self_sign(&cha_cert, authz->domain, domains, cha_key, 
+                               apr_time_from_sec(7 * MD_SECS_PER_DAY), p);
+        
+        if (APR_SUCCESS != rv) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: setup self signed cert for %s",
+                          authz->domain, cha_dns);
+            goto out;
+        }
+        
+        rv = md_store_save(store, p, MD_SG_CHALLENGES, cha_dns, MD_FN_TLSSNI01_PKEY,
+                           MD_SV_PKEY, (void*)cha_key, 0);
+        if (APR_SUCCESS == rv) {
+            rv = md_store_save(store, p, MD_SG_CHALLENGES, cha_dns, MD_FN_TLSSNI01_CERT,
+                               MD_SV_CERT, (void*)cha_cert, 0);
+        }
+        authz->dir = cha_dns;
+        notify_server = 1;
+    }
+    
+    if (APR_SUCCESS == rv && notify_server) {
+        authz_req_ctx ctx;
+
+        /* challenge is setup or was changed from previous data, tell ACME server
+         * so it may (re)try verification */        
+        authz_req_ctx_init(&ctx, acme, NULL, authz, p);
+        ctx.challenge = cha;
+        rv = md_acme_POST(acme, cha->uri, on_init_authz_resp, authz_http_set, NULL, &ctx);
+    }
+out:    
+    return rv;
+}
+
+typedef apr_status_t cha_starter(md_acme_authz_cha_t *cha, md_acme_authz_t *authz, 
+                                 md_acme_t *acme, md_store_t *store, 
+                                 md_pkey_spec_t *key_spec, apr_pool_t *p);
+                                 
+typedef struct {
+    const char *name;
+    cha_starter *start;
+} cha_type;
+
+static const cha_type CHA_TYPES[] = {
+    { MD_AUTHZ_TYPE_HTTP01,     cha_http_01_setup },
+    { MD_AUTHZ_TYPE_TLSSNI01,   cha_tls_sni_01_setup },
+};
+static const apr_size_t CHA_TYPES_LEN = (sizeof(CHA_TYPES)/sizeof(CHA_TYPES[0]));
+
+typedef struct {
+    apr_pool_t *p;
+    const char *type;
+    md_acme_authz_cha_t *accepted;
+    apr_array_header_t *offered;
+} cha_find_ctx;
+
+static apr_status_t collect_offered(void *baton, size_t index, md_json_t *json)
+{
+    cha_find_ctx *ctx = baton;
+    const char *ctype;
+    
+    (void)index;
+    if ((ctype = md_json_gets(json, MD_KEY_TYPE, NULL))) {
+        APR_ARRAY_PUSH(ctx->offered, const char*) = apr_pstrdup(ctx->p, ctype);
+    }
+    return 1;
+}
+
+static apr_status_t find_type(void *baton, size_t index, md_json_t *json)
+{
+    cha_find_ctx *ctx = baton;
+    
+    const char *ctype = md_json_gets(json, MD_KEY_TYPE, NULL);
+    if (ctype && !apr_strnatcasecmp(ctx->type, ctype)) {
+        ctx->accepted = cha_from_json(ctx->p, index, json);
+        return 0;
+    }
+    return 1;
+}
+
+apr_status_t md_acme_authz_respond(md_acme_authz_t *authz, md_acme_t *acme, md_store_t *store, 
+                                   apr_array_header_t *challenges, 
+                                   md_pkey_spec_t *key_spec, apr_pool_t *p)
+{
+    apr_status_t rv;
+    int i;
+    cha_find_ctx fctx;
+    
+    assert(acme);
+    assert(authz);
+    assert(authz->resource);
+
+    fctx.p = p;
+    fctx.accepted = NULL;
+    
+    /* Look in the order challenge types are defined */
+    for (i = 0; i < challenges->nelts && !fctx.accepted; ++i) {
+        fctx.type = APR_ARRAY_IDX(challenges, i, const char *);
+        md_json_itera(find_type, &fctx, authz->resource, MD_KEY_CHALLENGES, NULL);
+    }
+    
+    if (!fctx.accepted) {
+        rv = APR_EINVAL;
+        fctx.offered = apr_array_make(p, 5, sizeof(const char*));
+        md_json_itera(collect_offered, &fctx, authz->resource, MD_KEY_CHALLENGES, NULL);
+        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, 
+                      "%s: the server offers no ACME challenge that is configured "
+                      "for this MD. The server offered '%s' and available for this "
+                      "MD are: '%s' (via %s).",
+                      authz->domain, 
+                      apr_array_pstrcat(p, fctx.offered, ' '),
+                      apr_array_pstrcat(p, challenges, ' '),
+                      authz->location);
+        return rv;
+    }
+    
+    for (i = 0; i < (int)CHA_TYPES_LEN; ++i) {
+        if (!apr_strnatcasecmp(CHA_TYPES[i].name, fctx.accepted->type)) {
+            return CHA_TYPES[i].start(fctx.accepted, authz, acme, store, key_spec, p);
+        }
+    }
+    
+    rv = APR_ENOTIMPL;
+    md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, 
+                  "%s: no implementation found for challenge '%s'",
+                  authz->domain, fctx.accepted->type);
+    return rv;
+}
+
+/**************************************************************************************************/
+/* Delete an existing authz resource */
+
+typedef struct {
+    apr_pool_t *p;
+    md_acme_authz_t *authz;
+} del_ctx;
+
+static apr_status_t on_init_authz_del(md_acme_req_t *req, void *baton)
+{
+    md_json_t *jpayload;
+
+    (void)baton;
+    jpayload = md_json_create(req->p);
+    md_json_sets("deactivated", jpayload, MD_KEY_STATUS, NULL);
+    
+    return md_acme_req_body_init(req, jpayload);
+} 
+
+static apr_status_t authz_del(md_acme_t *acme, apr_pool_t *p, const apr_table_t *hdrs, 
+                              md_json_t *body, void *baton)
+{
+    authz_req_ctx *ctx = baton;
+    
+    (void)p;
+    (void)body;
+    (void)hdrs;
+    md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, ctx->p, "deleted authz %s", ctx->authz->location);
+    acme->acct = NULL;
+    return APR_SUCCESS;
+}
+
+apr_status_t md_acme_authz_del(md_acme_authz_t *authz, md_acme_t *acme, 
+                               md_store_t *store, apr_pool_t *p)
+{
+    authz_req_ctx ctx;
+    
+    (void)store;
+    ctx.p = p;
+    ctx.authz = authz;
+    
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "delete authz for %s from %s", 
+                  authz->domain, authz->location);
+    return md_acme_POST(acme, authz->location, on_init_authz_del, authz_del, NULL, &ctx);
+}
+
+/**************************************************************************************************/
+/* authz conversion */
+
+md_json_t *md_acme_authz_to_json(md_acme_authz_t *a, apr_pool_t *p)
+{
+    md_json_t *json = md_json_create(p);
+    if (json) {
+        md_json_sets(a->domain, json, MD_KEY_DOMAIN, NULL);
+        md_json_sets(a->location, json, MD_KEY_LOCATION, NULL);
+        md_json_sets(a->dir, json, MD_KEY_DIR, NULL);
+        md_json_setl(a->state, json, MD_KEY_STATE, NULL);
+        return json;
+    }
+    return NULL;
+}
+
+md_acme_authz_t *md_acme_authz_from_json(struct md_json_t *json, apr_pool_t *p)
+{
+    md_acme_authz_t *authz = md_acme_authz_create(p);
+    if (authz) {
+        authz->domain = md_json_dups(p, json, MD_KEY_DOMAIN, NULL);            
+        authz->location = md_json_dups(p, json, MD_KEY_LOCATION, NULL);            
+        authz->dir = md_json_dups(p, json, MD_KEY_DIR, NULL);            
+        authz->state = (md_acme_authz_state_t)md_json_getl(json, MD_KEY_STATE, NULL);            
+        return authz;
+    }
+    return NULL;
+}
+
+/**************************************************************************************************/
+/* authz_set conversion */
+
+#define MD_KEY_ACCOUNT          "account"
+#define MD_KEY_AUTHZS           "authorizations"
+
+static apr_status_t authz_to_json(void *value, md_json_t *json, apr_pool_t *p, void *baton)
+{
+    (void)baton;
+    return md_json_setj(md_acme_authz_to_json(value, p), json, NULL);
+}
+
+static apr_status_t authz_from_json(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton)
+{
+    (void)baton;
+    *pvalue = md_acme_authz_from_json(json, p);
+    return (*pvalue)? APR_SUCCESS : APR_EINVAL;
+}
+
+md_json_t *md_acme_authz_set_to_json(md_acme_authz_set_t *set, apr_pool_t *p)
+{
+    md_json_t *json = md_json_create(p);
+    if (json) {
+        md_json_seta(set->authzs, authz_to_json, NULL, json, MD_KEY_AUTHZS, NULL);
+        return json;
+    }
+    return NULL;
+}
+
+md_acme_authz_set_t *md_acme_authz_set_from_json(md_json_t *json, apr_pool_t *p)
+{
+    md_acme_authz_set_t *set = md_acme_authz_set_create(p);
+    if (set) {
+        md_json_geta(set->authzs, authz_from_json, NULL, json, MD_KEY_AUTHZS, NULL);
+        return set;
+    }
+    return NULL;
+}
+
+/**************************************************************************************************/
+/* persistence */
+
+apr_status_t md_acme_authz_set_load(struct md_store_t *store, md_store_group_t group, 
+                                    const char *md_name, md_acme_authz_set_t **pauthz_set, 
+                                    apr_pool_t *p)
+{
+    apr_status_t rv;
+    md_json_t *json;
+    md_acme_authz_set_t *authz_set;
+    
+    rv = md_store_load_json(store, group, md_name, MD_FN_AUTHZ, &json, p);
+    if (APR_SUCCESS == rv) {
+        authz_set = md_acme_authz_set_from_json(json, p);
+    }
+    *pauthz_set = (APR_SUCCESS == rv)? authz_set : NULL;
+    return rv;  
+}
+
+static apr_status_t p_save(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    md_store_t *store = baton;
+    md_json_t *json;
+    md_store_group_t group;
+    md_acme_authz_set_t *set;
+    const char *md_name;
+    int create;
+    (void)p;   
+    group = (md_store_group_t)va_arg(ap, int);
+    md_name = va_arg(ap, const char *);
+    set = va_arg(ap, md_acme_authz_set_t *);
+    create = va_arg(ap, int);
+
+    json = md_acme_authz_set_to_json(set, ptemp);
+    assert(json);
+    return md_store_save_json(store, ptemp, group, md_name, MD_FN_AUTHZ, json, create);
+}
+
+apr_status_t md_acme_authz_set_save(struct md_store_t *store, apr_pool_t *p,
+                                    md_store_group_t group, const char *md_name, 
+                                    md_acme_authz_set_t *authz_set, int create)
+{
+    return md_util_pool_vdo(p_save, store, p, group, md_name, authz_set, create, NULL);
+}
+
+static apr_status_t p_purge(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    md_store_t *store = baton;
+    md_acme_authz_set_t *authz_set;
+    const md_acme_authz_t *authz;
+    md_store_group_t group;
+    const char *md_name;
+    int i;
+
+    group = (md_store_group_t)va_arg(ap, int);
+    md_name = va_arg(ap, const char *);
+
+    if (APR_SUCCESS == md_acme_authz_set_load(store, group, md_name, &authz_set, p)) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "authz_set loaded for %s", md_name);
+        for (i = 0; i < authz_set->authzs->nelts; ++i) {
+            authz = APR_ARRAY_IDX(authz_set->authzs, i, const md_acme_authz_t*);
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "authz check %s", authz->domain);
+            if (authz->dir) {
+                md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "authz purge %s", authz->dir);
+                md_store_purge(store, p, MD_SG_CHALLENGES, authz->dir);
+            }
+        }
+    }
+    return md_store_remove(store, group, md_name, MD_FN_AUTHZ, ptemp, 1);
+}
+
+apr_status_t md_acme_authz_set_purge(md_store_t *store, md_store_group_t group,
+                                     apr_pool_t *p, const char *md_name)
+{
+    return md_util_pool_vdo(p_purge, store, p, group, md_name, NULL);
+}
+
diff --git a/modules/md/md_acme_authz.h b/modules/md/md_acme_authz.h
new file mode 100644 (file)
index 0000000..3b083a9
--- /dev/null
@@ -0,0 +1,103 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef mod_md_md_acme_authz_h
+#define mod_md_md_acme_authz_h
+
+struct apr_array_header_t;
+struct md_acme_t;
+struct md_acme_acct_t;
+struct md_json_t;
+struct md_store_t;
+struct md_pkey_spec_t;
+
+typedef struct md_acme_challenge_t md_acme_challenge_t;
+
+/**************************************************************************************************/
+/* authorization request for a specific domain name */
+
+#define MD_AUTHZ_TYPE_HTTP01        "http-01"
+#define MD_AUTHZ_TYPE_TLSSNI01      "tls-sni-01"
+
+typedef enum {
+    MD_ACME_AUTHZ_S_UNKNOWN,
+    MD_ACME_AUTHZ_S_PENDING,
+    MD_ACME_AUTHZ_S_VALID,
+    MD_ACME_AUTHZ_S_INVALID,
+} md_acme_authz_state_t;
+
+typedef struct md_acme_authz_t md_acme_authz_t;
+
+struct md_acme_authz_t {
+    const char *domain;
+    const char *location;
+    const char *dir;
+    md_acme_authz_state_t state;
+    apr_time_t expires;
+    struct md_json_t *resource;
+};
+
+#define MD_FN_HTTP01            "acme-http-01.txt"
+#define MD_FN_TLSSNI01_CERT     "acme-tls-sni-01.cert.pem"
+#define MD_FN_TLSSNI01_PKEY     "acme-tls-sni-01.key.pem"
+#define MD_FN_AUTHZ             "authz.json"
+
+
+md_acme_authz_t *md_acme_authz_create(apr_pool_t *p);
+
+struct md_json_t *md_acme_authz_to_json(md_acme_authz_t *a, apr_pool_t *p);
+md_acme_authz_t *md_acme_authz_from_json(struct md_json_t *json, apr_pool_t *p);
+
+/* authz interaction with ACME server */
+apr_status_t md_acme_authz_register(struct md_acme_authz_t **pauthz, struct md_acme_t *acme,
+                                    struct md_store_t *store, const char *domain, apr_pool_t *p);
+
+apr_status_t md_acme_authz_update(md_acme_authz_t *authz, struct md_acme_t *acme, 
+                                  struct md_store_t *store, apr_pool_t *p);
+
+apr_status_t md_acme_authz_respond(md_acme_authz_t *authz, struct md_acme_t *acme, 
+                                   struct md_store_t *store, apr_array_header_t *challenges, 
+                                   struct md_pkey_spec_t *key_spec, apr_pool_t *p);
+apr_status_t md_acme_authz_del(md_acme_authz_t *authz, struct md_acme_t *acme, 
+                               struct md_store_t *store, apr_pool_t *p);
+
+/**************************************************************************************************/
+/* set of authz data for a managed domain */
+
+typedef struct md_acme_authz_set_t md_acme_authz_set_t;
+
+struct md_acme_authz_set_t {
+    struct apr_array_header_t *authzs;
+};
+
+md_acme_authz_set_t *md_acme_authz_set_create(apr_pool_t *p);
+md_acme_authz_t *md_acme_authz_set_get(md_acme_authz_set_t *set, const char *domain);
+apr_status_t md_acme_authz_set_add(md_acme_authz_set_t *set, md_acme_authz_t *authz);
+apr_status_t md_acme_authz_set_remove(md_acme_authz_set_t *set, const char *domain);
+
+struct md_json_t *md_acme_authz_set_to_json(md_acme_authz_set_t *set, apr_pool_t *p);
+md_acme_authz_set_t *md_acme_authz_set_from_json(struct md_json_t *json, apr_pool_t *p);
+
+apr_status_t md_acme_authz_set_load(struct md_store_t *store, md_store_group_t group, 
+                                    const char *md_name, md_acme_authz_set_t **pauthz_set, 
+                                    apr_pool_t *p);
+apr_status_t md_acme_authz_set_save(struct md_store_t *store, apr_pool_t *p, 
+                                    md_store_group_t group, const char *md_name, 
+                                    md_acme_authz_set_t *authz_set, int create);
+
+apr_status_t md_acme_authz_set_purge(struct md_store_t *store, md_store_group_t group,
+                                     apr_pool_t *p, const char *md_name);
+
+#endif /* md_acme_authz_h */
diff --git a/modules/md/md_acme_drive.c b/modules/md/md_acme_drive.c
new file mode 100644 (file)
index 0000000..ccdb3e6
--- /dev/null
@@ -0,0 +1,1003 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+#include <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_buckets.h>
+#include <apr_hash.h>
+#include <apr_uri.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_jws.h"
+#include "md_http.h"
+#include "md_log.h"
+#include "md_reg.h"
+#include "md_store.h"
+#include "md_util.h"
+
+#include "md_acme.h"
+#include "md_acme_acct.h"
+#include "md_acme_authz.h"
+
+typedef struct {
+    md_proto_driver_t *driver;
+    
+    const char *phase;
+    int complete;
+
+    md_pkey_t *privkey;              /* the new private key */
+    apr_array_header_t *pubcert;     /* the new certificate + chain certs */
+    
+    md_cert_t *cert;                 /* the new certificate */
+    apr_array_header_t *chain;       /* the chain certificates */
+
+    md_acme_t *acme;
+    md_t *md;
+    const md_creds_t *ncreds;
+    
+    apr_array_header_t *ca_challenges;
+    md_acme_authz_set_t *authz_set;
+    apr_interval_time_t authz_monitor_timeout;
+    
+    const char *csr_der_64;
+    apr_interval_time_t cert_poll_timeout;
+    
+    const char *chain_url;
+    
+} md_acme_driver_t;
+
+/**************************************************************************************************/
+/* account setup */
+
+static apr_status_t ad_set_acct(md_proto_driver_t *d) 
+{
+    md_acme_driver_t *ad = d->baton;
+    md_t *md = ad->md;
+    apr_status_t rv = APR_SUCCESS;
+    int update = 0, acct_installed = 0;
+    
+    ad->phase = "setup acme";
+    if (!ad->acme 
+        && APR_SUCCESS != (rv = md_acme_create(&ad->acme, d->p, md->ca_url, d->proxy_url))) {
+        goto out;
+    }
+
+    ad->phase = "choose account";
+    /* Do we have a staged (modified) account? */
+    if (APR_SUCCESS == (rv = md_acme_use_acct_staged(ad->acme, d->store, md, d->p))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "re-using staged account");
+        md->ca_account = MD_ACME_ACCT_STAGED;
+        acct_installed = 1;
+    }
+    else if (APR_STATUS_IS_ENOENT(rv)) {
+        rv = APR_SUCCESS;
+    }
+    
+    /* Get an account for the ACME server for this MD */
+    if (md->ca_account && !acct_installed) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "re-use account '%s'", md->ca_account);
+        rv = md_acme_use_acct(ad->acme, d->store, d->p, md->ca_account);
+        if (APR_STATUS_IS_ENOENT(rv) || APR_STATUS_IS_EINVAL(rv)) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "rejected %s", md->ca_account);
+            md->ca_account = NULL;
+            update = 1;
+            rv = APR_SUCCESS;
+        }
+    }
+
+    if (APR_SUCCESS == rv && !md->ca_account) {
+        /* Find a local account for server, store at MD */ 
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: looking at existing accounts",
+                      d->proto->protocol);
+        if (APR_SUCCESS == md_acme_find_acct(ad->acme, d->store, d->p)) {
+            md->ca_account = md_acme_get_acct_id(ad->acme);
+            update = 1;
+        }
+    }
+    
+    if (APR_SUCCESS == rv && !md->ca_account) {
+        /* 2.2 No local account exists, create a new one */
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: creating new account", 
+                      d->proto->protocol);
+        
+        if (!ad->md->contacts || apr_is_empty_array(md->contacts)) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, d->p, 
+                          "no contact information for md %s", md->name);            
+            rv = APR_EINVAL;
+            goto out;
+        }
+    
+        if (APR_SUCCESS == (rv = md_acme_create_acct(ad->acme, d->p, md->contacts, 
+                                                     md->ca_agreement))
+            && APR_SUCCESS == (rv = md_acme_acct_save_staged(ad->acme, d->store, md, d->p))) {
+            md->ca_account = MD_ACME_ACCT_STAGED;
+            update = 1;
+        }
+    }
+    
+out:
+    if (APR_SUCCESS == rv) {
+        const char *agreement = md_acme_get_agreement(ad->acme);
+        /* Persist the account chosen at the md so we use the same on future runs */
+        if (agreement && !md->ca_agreement) { 
+            md->ca_agreement = agreement;
+            update = 1;
+        }
+        if (update) {
+            rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0);
+        }
+    }
+    return rv;
+}
+
+/**************************************************************************************************/
+/* authz/challenge setup */
+
+/**
+ * Pre-Req: we have an account for the ACME server that has accepted the current license agreement
+ * For each domain in MD: 
+ * - check if there already is a valid AUTHZ resource
+ * - if ot, create an AUTHZ resource with challenge data 
+ */
+static apr_status_t ad_setup_authz(md_proto_driver_t *d)
+{
+    md_acme_driver_t *ad = d->baton;
+    apr_status_t rv;
+    md_t *md = ad->md;
+    md_acme_authz_t *authz;
+    int i, changed;
+    
+    assert(ad->md);
+    assert(ad->acme);
+
+    ad->phase = "check authz";
+    
+    /* For each domain in MD: AUTHZ setup
+     * if an AUTHZ resource is known, check if it is still valid
+     * if known AUTHZ resource is not valid, remove, goto 4.1.1
+     * if no AUTHZ available, create a new one for the domain, store it
+     */
+    rv = md_acme_authz_set_load(d->store, MD_SG_STAGING, md->name, &ad->authz_set, d->p);
+    if (!ad->authz_set || APR_STATUS_IS_ENOENT(rv)) {
+        ad->authz_set = md_acme_authz_set_create(d->p);
+        rv = APR_SUCCESS;
+    }
+    else if (APR_SUCCESS != rv) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: loading authz data", md->name);
+        md_acme_authz_set_purge(d->store, MD_SG_STAGING, d->p, md->name);
+        return APR_EAGAIN;
+    }
+    
+    /* Remove anything we no longer need */
+    for (i = 0; i < ad->authz_set->authzs->nelts; ++i) {
+        authz = APR_ARRAY_IDX(ad->authz_set->authzs, i, md_acme_authz_t*);
+        if (!md_contains(md, authz->domain, 0)) {
+            md_acme_authz_set_remove(ad->authz_set, authz->domain);
+            changed = 1;
+        }
+    }
+    
+    /* Add anything we do not already have */
+    for (i = 0; i < md->domains->nelts && APR_SUCCESS == rv; ++i) {
+        const char *domain = APR_ARRAY_IDX(md->domains, i, const char *);
+        changed = 0;
+        authz = md_acme_authz_set_get(ad->authz_set, domain);
+        if (authz) {
+            /* check valid */
+            rv = md_acme_authz_update(authz, ad->acme, d->store, d->p);
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: updated authz for %s", 
+                          md->name, domain);
+            if (APR_SUCCESS != rv) {
+                md_acme_authz_set_remove(ad->authz_set, domain);
+                authz = NULL;
+                changed = 1;
+            }
+        }
+        if (!authz) {
+            /* create new one */
+            rv = md_acme_authz_register(&authz, ad->acme, d->store, domain, d->p);
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: created authz for %s", 
+                          md->name, domain);
+            if (APR_SUCCESS == rv) {
+                rv = md_acme_authz_set_add(ad->authz_set, authz);
+                changed = 1;
+            }
+        }
+    }
+    
+    /* Save any changes */
+    if (APR_SUCCESS == rv && changed) {
+        rv = md_acme_authz_set_save(d->store, d->p, MD_SG_STAGING, md->name, ad->authz_set, 0);
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, "%s: saved", md->name);
+    }
+    
+    return rv;
+}
+
+/**
+ * Pre-Req: all domains have a AUTHZ resources at the ACME server
+ * For each domain in MD: 
+ * - if AUTHZ resource is 'valid' -> continue
+ * - if AUTHZ resource is 'pending':
+ *   - find preferred challenge choice
+ *   - calculate challenge data for httpd to find
+ *   - POST challenge start to ACME server
+ * For each domain in MD where AUTHZ is 'pending', until overall timeout: 
+ *   - wait a certain time, check status again
+ * If not all AUTHZ are valid, fail
+ */
+static apr_status_t ad_start_challenges(md_proto_driver_t *d)
+{
+    md_acme_driver_t *ad = d->baton;
+    apr_status_t rv = APR_SUCCESS;
+    md_acme_authz_t *authz;
+    int i, changed = 0;
+    
+    assert(ad->md);
+    assert(ad->acme);
+    assert(ad->authz_set);
+
+    ad->phase = "start challenges";
+
+    for (i = 0; i < ad->authz_set->authzs->nelts && APR_SUCCESS == rv; ++i) {
+        authz = APR_ARRAY_IDX(ad->authz_set->authzs, i, md_acme_authz_t*);
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: check AUTHZ for %s", 
+                      ad->md->name, authz->domain);
+        if (APR_SUCCESS != (rv = md_acme_authz_update(authz, ad->acme, d->store, d->p))) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: check authz for %s",
+                          ad->md->name, authz->domain);
+            break;
+        }
+
+        switch (authz->state) {
+            case MD_ACME_AUTHZ_S_VALID:
+                break;
+                
+            case MD_ACME_AUTHZ_S_PENDING:
+                rv = md_acme_authz_respond(authz, ad->acme, d->store, ad->ca_challenges, 
+                                           d->md->pkey_spec, d->p);
+                changed = 1;
+                break;
+                
+            default:
+                rv = APR_EINVAL;
+                md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, 
+                              "%s: unexpected AUTHZ state %d at %s", 
+                              authz->domain, authz->state, authz->location);
+                break;
+        }
+    }
+    
+    if (APR_SUCCESS == rv && changed) {
+        rv = md_acme_authz_set_save(d->store, d->p, MD_SG_STAGING, ad->md->name, ad->authz_set, 0);
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, "%s: saved", ad->md->name);
+    }
+    return rv;
+}
+
+static apr_status_t check_challenges(void *baton, int attempt)
+{
+    md_proto_driver_t *d = baton;
+    md_acme_driver_t *ad = d->baton;
+    md_acme_authz_t *authz;
+    apr_status_t rv = APR_SUCCESS;
+    int i;
+    
+    for (i = 0; i < ad->authz_set->authzs->nelts && APR_SUCCESS == rv; ++i) {
+        authz = APR_ARRAY_IDX(ad->authz_set->authzs, i, md_acme_authz_t*);
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: check AUTHZ for %s(%d. attempt)", 
+                      ad->md->name, authz->domain, attempt);
+        if (APR_SUCCESS == (rv = md_acme_authz_update(authz, ad->acme, d->store, d->p))) {
+            switch (authz->state) {
+                case MD_ACME_AUTHZ_S_VALID:
+                    break;
+                case MD_ACME_AUTHZ_S_PENDING:
+                    rv = APR_EAGAIN;
+                    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, 
+                                  "%s: status pending at %s", authz->domain, authz->location);
+                    break;
+                default:
+                    rv = APR_EINVAL;
+                    md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, 
+                                  "%s: unexpected AUTHZ state %d at %s", 
+                                  authz->domain, authz->state, authz->location);
+                    break;
+            }
+        }
+    }
+    return rv;
+}
+
+static apr_status_t ad_monitor_challenges(md_proto_driver_t *d)
+{
+    md_acme_driver_t *ad = d->baton;
+    apr_status_t rv;
+    
+    assert(ad->md);
+    assert(ad->acme);
+    assert(ad->authz_set);
+
+    ad->phase = "monitor challenges";
+    rv = md_util_try(check_challenges, d, 0, ad->authz_monitor_timeout, 0, 0, 1);
+    
+    md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, d->p, 
+                  "%s: checked all domain authorizations", ad->md->name);
+    return rv;
+}
+
+/**************************************************************************************************/
+/* poll cert */
+
+
+static apr_status_t read_http_cert(md_cert_t **pcert, apr_pool_t *p,
+                                   const md_http_response_t *res)
+{
+    apr_status_t rv = APR_SUCCESS;
+    
+    if (APR_SUCCESS != (rv = md_cert_read_http(pcert, p, res))
+        && APR_STATUS_IS_ENOENT(rv)) {
+        rv = APR_EAGAIN;
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, 
+                      "cert not in response from %s", res->req->url);
+    }
+    return rv;
+}
+
+static apr_status_t on_got_cert(md_acme_t *acme, const md_http_response_t *res, void *baton)
+{
+    md_proto_driver_t *d = baton;
+    md_acme_driver_t *ad = d->baton;
+    apr_status_t rv = APR_SUCCESS;
+    
+    (void)acme;
+    if (APR_SUCCESS == (rv = read_http_cert(&ad->cert, d->p, res))) {
+        rv = md_store_save(d->store, d->p, MD_SG_STAGING, ad->md->name, MD_FN_CERT, 
+                           MD_SV_CERT, ad->cert, 0);
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "cert parsed and saved");
+    }
+    return rv;
+}
+
+static apr_status_t get_cert(void *baton, int attempt)
+{
+    md_proto_driver_t *d = baton;
+    md_acme_driver_t *ad = d->baton;
+    
+    (void)attempt;
+    return md_acme_GET(ad->acme, ad->md->cert_url, NULL, NULL, on_got_cert, d);
+}
+
+static apr_status_t ad_cert_poll(md_proto_driver_t *d, int only_once)
+{
+    md_acme_driver_t *ad = d->baton;
+    apr_status_t rv;
+    
+    assert(ad->md);
+    assert(ad->acme);
+    assert(ad->md->cert_url);
+    
+    ad->phase = "poll certificate";
+    if (only_once) {
+        rv = get_cert(d, 0);
+    }
+    else {
+        rv = md_util_try(get_cert, d, 1, ad->cert_poll_timeout, 0, 0, 1);
+    }
+    
+    md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "poll for cert at %s", ad->md->cert_url);
+    return rv;
+}
+
+/**************************************************************************************************/
+/* cert setup */
+
+static apr_status_t on_init_csr_req(md_acme_req_t *req, void *baton)
+{
+    md_proto_driver_t *d = baton;
+    md_acme_driver_t *ad = d->baton;
+    md_json_t *jpayload;
+
+    jpayload = md_json_create(req->p);
+    md_json_sets("new-cert", jpayload, MD_KEY_RESOURCE, NULL);
+    md_json_sets(ad->csr_der_64, jpayload, MD_KEY_CSR, NULL);
+    
+    return md_acme_req_body_init(req, jpayload);
+} 
+
+static apr_status_t csr_req(md_acme_t *acme, const md_http_response_t *res, void *baton)
+{
+    md_proto_driver_t *d = baton;
+    md_acme_driver_t *ad = d->baton;
+    apr_status_t rv = APR_SUCCESS;
+    
+    (void)acme;
+    ad->md->cert_url = apr_table_get(res->headers, "location");
+    if (!ad->md->cert_url) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, d->p, 
+                      "cert created without giving its location header");
+        return APR_EINVAL;
+    }
+    if (APR_SUCCESS != (rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, d->p, 
+                      "%s: saving cert url %s", ad->md->name, ad->md->cert_url);
+        return rv;
+    }
+    
+    /* Check if it already was sent with this response */
+    if (APR_SUCCESS == (rv = md_cert_read_http(&ad->cert, d->p, res))) {
+        rv = md_cert_save(d->store, d->p, MD_SG_STAGING, ad->md->name, ad->cert, 0);
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "cert parsed and saved");
+    }
+    else if (APR_STATUS_IS_ENOENT(rv)) {
+        rv = APR_SUCCESS;
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, 
+                      "cert not in response, need to poll %s", ad->md->cert_url);
+    }
+    
+    return rv;
+}
+
+/**
+ * Pre-Req: all domains have been validated by the ACME server, e.g. all have AUTHZ
+ * resources that have status 'valid'
+ * - Setup private key, if not already there
+ * - Generate a CSR with org, contact, etc
+ * - Optionally enable must-staple OCSP extension
+ * - Submit CSR, expect 201 with location
+ * - POLL location for certificate
+ * - store certificate
+ * - retrieve cert chain information from cert
+ * - GET cert chain
+ * - store cert chain
+ */
+static apr_status_t ad_setup_certificate(md_proto_driver_t *d)
+{
+    md_acme_driver_t *ad = d->baton;
+    md_pkey_t *privkey;
+    apr_status_t rv;
+
+    ad->phase = "setup cert privkey";
+    
+    rv = md_pkey_load(d->store, MD_SG_STAGING, ad->md->name, &privkey, d->p);
+    if (APR_STATUS_IS_ENOENT(rv)) {
+        if (APR_SUCCESS == (rv = md_pkey_gen(&privkey, d->p, d->md->pkey_spec))) {
+            rv = md_pkey_save(d->store, d->p, MD_SG_STAGING, ad->md->name, privkey, 1);
+        }
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: generate privkey", ad->md->name);
+    }
+
+    if (APR_SUCCESS == rv) {
+        ad->phase = "setup csr";
+        rv = md_cert_req_create(&ad->csr_der_64, ad->md, privkey, d->p);
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: create CSR", ad->md->name);
+    }
+
+    if (APR_SUCCESS == rv) {
+        ad->phase = "submit csr";
+        rv = md_acme_POST(ad->acme, ad->acme->new_cert, on_init_csr_req, NULL, csr_req, d);
+    }
+
+    if (APR_SUCCESS == rv) {
+        if (!ad->cert) {
+            rv = ad_cert_poll(d, 0);
+        }
+    }
+    return rv;
+}
+
+/**************************************************************************************************/
+/* cert chain retrieval */
+
+static apr_status_t on_add_chain(md_acme_t *acme, const md_http_response_t *res, void *baton)
+{
+    md_proto_driver_t *d = baton;
+    md_acme_driver_t *ad = d->baton;
+    apr_status_t rv = APR_SUCCESS;
+    md_cert_t *cert;
+    const char *ct;
+    
+    (void)acme;
+    ct = apr_table_get(res->headers, "Content-Type");
+    if (ct && !strcmp("application/x-pkcs7-mime", ct)) {
+        /* root cert most likely, end it here */
+        return APR_SUCCESS;
+    }
+    
+    if (APR_SUCCESS == (rv = read_http_cert(&cert, d->p, res))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "chain cert parsed");
+        APR_ARRAY_PUSH(ad->chain, md_cert_t *) = cert;
+    }
+    return rv;
+}
+
+static apr_status_t get_chain(void *baton, int attempt)
+{
+    md_proto_driver_t *d = baton;
+    md_acme_driver_t *ad = d->baton;
+    md_cert_t *cert;
+    const char *url, *last_url = NULL;
+    apr_status_t rv = APR_SUCCESS;
+
+    while (APR_SUCCESS == rv && ad->chain->nelts < 10) {
+        int nelts = ad->chain->nelts;
+        if (ad->chain && nelts > 0) {
+            cert = APR_ARRAY_IDX(ad->chain, nelts - 1, md_cert_t *);
+        }
+        else {
+            cert = ad->cert;
+        }
+        
+        if (APR_SUCCESS == (rv = md_cert_get_issuers_uri(&url, cert, d->p))
+            && (!last_url || strcmp(last_url, url))) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "next issuer is  %s", url);
+#if MD_EXPERIMENTAL
+            if (!strncmp("http://127.0.0.1:", url, sizeof("http://127.0.0.1:")-1)) {
+                /* test boulder instance always reports issuer cert on localhost, but we
+                 * may use a different address to reach the boulder server */
+                apr_uri_t curi, ca;
+                
+                if (APR_SUCCESS == apr_uri_parse(d->p, url, &curi)
+                    && APR_SUCCESS == apr_uri_parse(d->p, ad->acme->url, &ca)) {
+                    url = apr_psprintf(d->p, "%s://%s:%s%s", 
+                                       ca.scheme, ca.hostname, ca.port_str, curi.path);
+                }
+            }
+#endif
+            rv = md_acme_GET(ad->acme, url, NULL, NULL, on_add_chain, d);
+            
+            if (APR_SUCCESS == rv && nelts == ad->chain->nelts) {
+                break;
+            }
+        }
+        else if (APR_STATUS_IS_ENOENT(rv) || !url || !strlen(url)) {
+            rv = APR_SUCCESS;
+            break;
+        }
+    }
+    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, 
+                  "got chain with %d certs (%d. attempt)", ad->chain->nelts, attempt);
+    return rv;
+}
+
+static apr_status_t ad_chain_install(md_proto_driver_t *d)
+{
+    md_acme_driver_t *ad = d->baton;
+    apr_status_t rv;
+    
+    ad->chain = apr_array_make(d->p, 5, sizeof(md_cert_t *));
+    if (APR_SUCCESS == (rv = md_util_try(get_chain, d, 0, ad->cert_poll_timeout, 0, 0, 0))) {
+        rv = md_store_save(d->store, d->p, MD_SG_STAGING, ad->md->name, MD_FN_CHAIN, 
+                           MD_SV_CHAIN, ad->chain, 0);
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "chain fetched and saved");
+    }
+    return rv;
+}
+
+/**************************************************************************************************/
+/* ACME driver init */
+
+static apr_status_t acme_driver_init(md_proto_driver_t *d)
+{
+    md_acme_driver_t *ad;
+    apr_status_t rv = APR_SUCCESS;
+
+    ad = apr_pcalloc(d->p, sizeof(*ad));
+    
+    d->baton = ad;
+    ad->driver = d;
+    
+    ad->authz_monitor_timeout = apr_time_from_sec(30);
+    ad->cert_poll_timeout = apr_time_from_sec(30);
+
+    /* We can only support challenges if the server is reachable from the outside
+     * via port 80 and/or 443. These ports might be mapped for httpd to something
+     * else, but a mapping needs to exist. */
+    ad->ca_challenges = apr_array_make(d->p, 3, sizeof(const char *)); 
+    if (d->challenge) {
+        /* we have been told to use this type */
+        APR_ARRAY_PUSH(ad->ca_challenges, const char*) = apr_pstrdup(d->p, d->challenge);
+    }
+    else if (d->md->ca_challenges && d->md->ca_challenges->nelts > 0) {
+        /* pre-configured set for this managed domain */
+        apr_array_cat(ad->ca_challenges, d->md->ca_challenges);
+    }
+    else {
+        /* free to chose. Add all we support and see what we get offered */
+        APR_ARRAY_PUSH(ad->ca_challenges, const char*) = MD_AUTHZ_TYPE_HTTP01;
+        APR_ARRAY_PUSH(ad->ca_challenges, const char*) = MD_AUTHZ_TYPE_TLSSNI01;
+    }
+    
+    if (!d->can_http && !d->can_https) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, d->p, "%s: the server seems neither "
+                      "reachable via http (port 80) nor https (port 443). The ACME protocol "
+                      "needs at least one of those so the CA can talk to the server and verify "
+                      "a domain ownership.", d->md->name);
+        return APR_EGENERAL;
+    }
+    
+    if (!d->can_http) {
+        ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_HTTP01, 0);
+    }
+    if (!d->can_https) {
+        ad->ca_challenges = md_array_str_remove(d->p, ad->ca_challenges, MD_AUTHZ_TYPE_TLSSNI01, 0);
+    }
+
+    if (apr_is_empty_array(ad->ca_challenges)) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, d->p, "%s: specific CA challenge methods "
+                      "have been configured, but the server is unable to use any of those. "
+                      "For 'http-01' it needs to be reachable on port 80, for 'tls-sni-01'"
+                      " port 443 is needed.", d->md->name);
+        return APR_EGENERAL;
+    }
+    
+    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, d->p, "%s: init driver", d->md->name);
+    
+    return rv;
+}
+
+/**************************************************************************************************/
+/* ACME staging */
+
+static apr_status_t acme_stage(md_proto_driver_t *d)
+{
+    md_acme_driver_t *ad = d->baton;
+    int reset_staging = d->reset;
+    apr_status_t rv = APR_SUCCESS;
+    int renew = 1;
+
+    if (md_log_is_level(d->p, MD_LOG_DEBUG)) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, d->p, "%s: staging started, "
+                      "state=%d, can_http=%d, can_https=%d, challenges='%s'",
+                      d->md->name, d->md->state, d->can_http, d->can_https,
+                      apr_array_pstrcat(d->p, ad->ca_challenges, ' '));
+    }
+
+    if (!reset_staging) {
+        rv = md_load(d->store, MD_SG_STAGING, d->md->name, &ad->md, d->p);
+        if (APR_SUCCESS == rv) {
+            /* So, we have a copy in staging, but is it a recent or an old one? */
+            if (md_is_newer(d->store, MD_SG_DOMAINS, MD_SG_STAGING, d->md->name, d->p)) {
+                reset_staging = 1;
+            }
+        }
+        else if (APR_STATUS_IS_ENOENT(rv)) {
+            reset_staging = 1;
+            rv = APR_SUCCESS;
+        }
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, rv, d->p, 
+                      "%s: checked staging area, will%s reset",
+                      d->md->name, reset_staging? "" : " not");
+    }
+    
+    if (reset_staging) {
+        /* reset the staging area for this domain */
+        rv = md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name);
+        if (APR_SUCCESS != rv && !APR_STATUS_IS_ENOENT(rv)) {
+            return rv;
+        }
+        rv = APR_SUCCESS;
+        ad->md = NULL;
+    }
+    
+    if (ad->md && ad->md->state == MD_S_MISSING) {
+        /* There is config information missing. It makes no sense to drive this MD further */
+        rv = APR_INCOMPLETE;
+        goto out;
+    }
+    
+    if (ad->md) {
+        /* staging in progress. look for new ACME account information collected there */
+        rv = md_reg_creds_get(&ad->ncreds, d->reg, MD_SG_STAGING, d->md, d->p);
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: checked creds", d->md->name);
+        if (APR_STATUS_IS_ENOENT(rv)) {
+            rv = APR_SUCCESS;
+        }
+    }
+    
+    /* Find out where we're at with this managed domain */
+    if (ad->ncreds && ad->ncreds->privkey && ad->ncreds->pubcert) {
+        /* There is a full set staged, to be loaded */
+        md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "%s: all data staged", d->md->name);
+        renew = 0;
+    }
+    
+    if (renew) {
+        if (APR_SUCCESS != (rv = md_acme_create(&ad->acme, d->p, d->md->ca_url, d->proxy_url)) 
+            || APR_SUCCESS != (rv = md_acme_setup(ad->acme))) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, "%s: setup ACME(%s)", 
+                          d->md->name, d->md->ca_url);
+            return rv;
+        }
+
+        if (!ad->md) {
+            /* re-initialize staging */
+            md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "%s: setup staging", d->md->name);
+            md_store_purge(d->store, d->p, MD_SG_STAGING, d->md->name);
+            ad->md = md_copy(d->p, d->md);
+            ad->md->cert_url = NULL; /* do not retrieve the old cert */
+            rv = md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0);
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: save staged md", 
+                          ad->md->name);
+        }
+
+        if (APR_SUCCESS == rv && !ad->cert) {
+            md_cert_load(d->store, MD_SG_STAGING, ad->md->name, &ad->cert, d->p);
+        }
+
+        if (APR_SUCCESS == rv && !ad->cert) {
+            ad->phase = "get certificate";
+            md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, "%s: need certificate", d->md->name);
+            
+            /* Chose (or create) and ACME account to use */
+            rv = ad_set_acct(d);
+            
+            /* Check that the account agreed to the terms-of-service, otherwise
+             * requests for new authorizations are denied. ToS may change during the
+             * lifetime of an account */
+            if (APR_SUCCESS == rv) {
+                const char *required;
+                
+                ad->phase = "check agreement";
+                md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, 
+                              "%s: check Terms-of-Service agreement", d->md->name);
+                
+                rv = md_acme_check_agreement(ad->acme, d->p, ad->md->ca_agreement, &required);
+                
+                if (APR_STATUS_IS_INCOMPLETE(rv) && required) {
+                    /* The CA wants the user to agree to Terms-of-Services. Until the user
+                     * has reconfigured and restarted the server, this MD cannot be
+                     * driven further */
+                    ad->md->state = MD_S_MISSING;
+                    md_save(d->store, d->p, MD_SG_STAGING, ad->md, 0);
+
+                    md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, d->p, 
+                                  "%s: the CA requires you to accept the terms-of-service "
+                                  "as specified in <%s>. "
+                                  "Please read the document that you find at that URL and, "
+                                  "if you agree to the conditions, configure "
+                                  "\"MDCertificateAgreement url\" "
+                                  "with exactly that URL in your Apache. "
+                                  "Then (graceful) restart the server to activate.", 
+                                  ad->md->name, required);
+                    goto out;
+                }
+            }
+            
+            /* If we know a cert's location, try to get it. Previous download might
+             * have failed. If server 404 it, we clear our memory of it. */
+            if (APR_SUCCESS == rv && ad->md->cert_url) {
+                md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, 
+                              "%s: polling certificate", d->md->name);
+                rv = ad_cert_poll(d, 1);
+                if (APR_STATUS_IS_ENOENT(rv)) {
+                    /* Server reports to know nothing about it. */
+                    ad->md->cert_url = NULL;
+                    rv = md_reg_update(d->reg, d->p, ad->md->name, ad->md, MD_UPD_CERT_URL);
+                }
+            }
+            
+            if (APR_SUCCESS == rv && !ad->cert) {
+                md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, 
+                              "%s: setup new authorization", d->md->name);
+                if (APR_SUCCESS != (rv = ad_setup_authz(d))) {
+                    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: setup authz resource", 
+                                  ad->md->name);
+                    goto out;
+                }
+                md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, 
+                              "%s: setup new challenges", d->md->name);
+                if (APR_SUCCESS != (rv = ad_start_challenges(d))) {
+                    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: start challenges", 
+                                  ad->md->name);
+                    goto out;
+                }
+                md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, 
+                              "%s: monitoring challenge status", d->md->name);
+                if (APR_SUCCESS != (rv = ad_monitor_challenges(d))) {
+                    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: monitor challenges", 
+                                  ad->md->name);
+                    goto out;
+                }
+                md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, 
+                              "%s: creating certificate request", d->md->name);
+                if (APR_SUCCESS != (rv = ad_setup_certificate(d))) {
+                    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: setup certificate", 
+                                  ad->md->name);
+                    goto out;
+                }
+                md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, 
+                              "%s: received certificate", d->md->name);
+            }
+            
+        }
+        
+        if (APR_SUCCESS == rv && !ad->chain) {
+            /* have we created this already? */
+            md_chain_load(d->store, MD_SG_STAGING, ad->md->name, &ad->chain, d->p);
+        }
+        if (APR_SUCCESS == rv && !ad->chain) {
+            ad->phase = "install chain";
+            md_log_perror(MD_LOG_MARK, MD_LOG_INFO, 0, d->p, 
+                          "%s: retrieving certificate chain", d->md->name);
+            rv = ad_chain_install(d);
+        }
+
+        if (APR_SUCCESS == rv && !ad->pubcert) {
+            /* have we created this already? */
+            md_pubcert_load(d->store, MD_SG_STAGING, ad->md->name, &ad->pubcert, d->p);
+        }
+        if (APR_SUCCESS == rv && !ad->pubcert) {
+            /* combine cert + chain into the pubcert */
+            ad->pubcert = apr_array_make(d->p, ad->chain->nelts + 1, sizeof(md_cert_t*));
+            APR_ARRAY_PUSH(ad->pubcert, md_cert_t *) = ad->cert;
+            apr_array_cat(ad->pubcert, ad->chain);
+            rv = md_pubcert_save(d->store, d->p, MD_SG_STAGING, ad->md->name, ad->pubcert, 0);
+        }
+
+        if (APR_SUCCESS == rv && ad->cert) {
+            apr_time_t now = apr_time_now();
+            apr_interval_time_t max_delay, delay_activation; 
+            
+            /* determine when this cert should be activated */
+            d->stage_valid_from = md_cert_get_not_before(ad->cert);
+            if (d->md->state == MD_S_COMPLETE && d->md->expires > now) {            
+                /**
+                 * The MD is complete and un-expired. This is a renewal run. 
+                 * Give activation 24 hours leeway (if we have that time) to
+                 * accomodate for clients with somewhat weird clocks.
+                 */
+                delay_activation = apr_time_from_sec(MD_SECS_PER_DAY);
+                if (delay_activation > (max_delay = d->md->expires - now)) {
+                    delay_activation = max_delay;
+                }
+                d->stage_valid_from += delay_activation;
+            }
+        }
+    }
+out:    
+    return rv;
+}
+
+static apr_status_t acme_driver_stage(md_proto_driver_t *d)
+{
+    md_acme_driver_t *ad = d->baton;
+    apr_status_t rv;
+
+    ad->phase = "ACME staging";
+    if (APR_SUCCESS == (rv = acme_stage(d))) {
+        ad->phase = "staging done";
+    }
+        
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: %s, %s", 
+                  d->md->name, d->proto->protocol, ad->phase);
+    return rv;
+}
+
+/**************************************************************************************************/
+/* ACME preload */
+
+static apr_status_t acme_preload(md_store_t *store, md_store_group_t load_group, 
+                                 const char *name, const char *proxy_url, apr_pool_t *p) 
+{
+    apr_status_t rv;
+    md_pkey_t *privkey, *acct_key;
+    md_t *md;
+    apr_array_header_t *pubcert;
+    struct md_acme_acct_t *acct;
+
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "%s: preload start", name);
+    /* Load all data which will be taken into the DOMAIN storage group.
+     * This serves several purposes:
+     *  1. It's a format check on the input data. 
+     *  2. We write back what we read, creating data with our own access permissions
+     *  3. We ignore any other accumulated data in STAGING
+     *  4. Once TMP is verified, we can swap/archive groups with a rename
+     *  5. Reading/Writing the data will apply/remove any group specific data encryption.
+     *     With the exemption that DOMAINS and TMP must apply the same policy/keys.
+     */
+    if (APR_SUCCESS != (rv = md_load(store, MD_SG_STAGING, name, &md, p))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: loading md json", name);
+        return rv;
+    }
+    if (APR_SUCCESS != (rv = md_pkey_load(store, MD_SG_STAGING, name, &privkey, p))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: loading staging private key", name);
+        return rv;
+    }
+    if (APR_SUCCESS != (rv = md_pubcert_load(store, MD_SG_STAGING, name, &pubcert, p))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: loading pubcert", name);
+        return rv;
+    }
+
+    /* See if staging holds a new or modified account data */
+    rv = md_acme_acct_load(&acct, &acct_key, store, MD_SG_STAGING, name, p);
+    if (APR_STATUS_IS_ENOENT(rv)) {
+        acct = NULL;
+        acct_key = NULL;
+        rv = APR_SUCCESS;
+    }
+    else if (APR_SUCCESS != rv) {
+        return rv; 
+    }
+
+    /* Remove any authz information we have here or in MD_SG_CHALLENGES */
+    md_acme_authz_set_purge(store, MD_SG_STAGING, p, name);
+
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, 
+                  "%s: staged data load, purging tmp space", name);
+    rv = md_store_purge(store, p, load_group, name);
+    if (APR_SUCCESS != rv) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: error purging preload storage", name);
+        return rv;
+    }
+    
+    if (acct) {
+        md_acme_t *acme;
+        
+        if (APR_SUCCESS != (rv = md_acme_create(&acme, p, md->ca_url, proxy_url))
+            || APR_SUCCESS != (rv = md_acme_acct_save(store, p, acme, acct, acct_key))) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: error saving acct", name);
+            return rv;
+        }
+        md->ca_account = acct->id;
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "%s: saved ACME account %s", 
+                      name, acct->id);
+    }
+    
+    if (APR_SUCCESS != (rv = md_save(store, p, load_group, md, 1))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: saving md json", name);
+        return rv;
+    }
+    if (APR_SUCCESS != (rv = md_pubcert_save(store, p, load_group, name, pubcert, 1))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: saving cert chain", name);
+        return rv;
+    }
+    if (APR_SUCCESS != (rv = md_pkey_save(store, p, load_group, name, privkey, 1))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: saving private key", name);
+        return rv;
+    }
+    
+    return rv;
+}
+
+static apr_status_t acme_driver_preload(md_proto_driver_t *d, md_store_group_t group)
+{
+    md_acme_driver_t *ad = d->baton;
+    apr_status_t rv;
+
+    ad->phase = "ACME preload";
+    if (APR_SUCCESS == (rv = acme_preload(d->store, group, d->md->name, d->proxy_url, d->p))) {
+        ad->phase = "preload done";
+    }
+        
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, d->p, "%s: %s, %s", 
+                  d->md->name, d->proto->protocol, ad->phase);
+    return rv;
+}
+
+static md_proto_t ACME_PROTO = {
+    MD_PROTO_ACME, acme_driver_init, acme_driver_stage, acme_driver_preload
+};
+apr_status_t md_acme_protos_add(apr_hash_t *protos, apr_pool_t *p)
+{
+    (void)p;
+    apr_hash_set(protos, MD_PROTO_ACME, sizeof(MD_PROTO_ACME)-1, &ACME_PROTO);
+    return APR_SUCCESS;
+}
diff --git a/modules/md/md_core.c b/modules/md/md_core.c
new file mode 100644 (file)
index 0000000..c079000
--- /dev/null
@@ -0,0 +1,427 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+#include <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_tables.h>
+#include <apr_time.h>
+#include <apr_date.h>
+
+#include "md_json.h"
+#include "md.h"
+#include "md_crypt.h"
+#include "md_log.h"
+#include "md_store.h"
+#include "md_util.h"
+
+
+int md_contains(const md_t *md, const char *domain, int case_sensitive)
+{
+   return md_array_str_index(md->domains, domain, 0, case_sensitive) >= 0;
+}
+
+const char *md_common_name(const md_t *md1, const md_t *md2)
+{
+    int i;
+    
+    if (md1 == NULL || md1->domains == NULL
+        || md2 == NULL || md2->domains == NULL) {
+        return NULL;
+    }
+    
+    for (i = 0; i < md1->domains->nelts; ++i) {
+        const char *name1 = APR_ARRAY_IDX(md1->domains, i, const char*);
+        if (md_contains(md2, name1, 0)) {
+            return name1;
+        }
+    }
+    return NULL;
+}
+
+int md_domains_overlap(const md_t *md1, const md_t *md2)
+{
+    return md_common_name(md1, md2) != NULL;
+}
+
+apr_size_t md_common_name_count(const md_t *md1, const md_t *md2)
+{
+    int i;
+    apr_size_t hits;
+    
+    if (md1 == NULL || md1->domains == NULL
+        || md2 == NULL || md2->domains == NULL) {
+        return 0;
+    }
+    
+    hits = 0;
+    for (i = 0; i < md1->domains->nelts; ++i) {
+        const char *name1 = APR_ARRAY_IDX(md1->domains, i, const char*);
+        if (md_contains(md2, name1, 0)) {
+            ++hits;
+        }
+    }
+    return hits;
+}
+
+md_t *md_create_empty(apr_pool_t *p)
+{
+    md_t *md = apr_pcalloc(p, sizeof(*md));
+    if (md) {
+        md->domains = apr_array_make(p, 5, sizeof(const char *));
+        md->contacts = apr_array_make(p, 5, sizeof(const char *));
+        md->drive_mode = MD_DRIVE_DEFAULT;
+        md->require_https = MD_REQUIRE_UNSET;
+        md->must_staple = -1;
+        md->transitive = -1;
+        md->defn_name = "unknown";
+        md->defn_line_number = 0;
+    }
+    return md;
+}
+
+int md_equal_domains(const md_t *md1, const md_t *md2, int case_sensitive)
+{
+    int i;
+    if (md1->domains->nelts == md2->domains->nelts) {
+        for (i = 0; i < md1->domains->nelts; ++i) {
+            const char *name1 = APR_ARRAY_IDX(md1->domains, i, const char*);
+            if (!md_contains(md2, name1, case_sensitive)) {
+                return 0;
+            }
+        }
+        return 1;
+    }
+    return 0;
+}
+
+int md_contains_domains(const md_t *md1, const md_t *md2)
+{
+    int i;
+    if (md1->domains->nelts >= md2->domains->nelts) {
+        for (i = 0; i < md2->domains->nelts; ++i) {
+            const char *name2 = APR_ARRAY_IDX(md2->domains, i, const char*);
+            if (!md_contains(md1, name2, 0)) {
+                return 0;
+            }
+        }
+        return 1;
+    }
+    return 0;
+}
+
+md_t *md_find_closest_match(apr_array_header_t *mds, const md_t *md)
+{
+    md_t *candidate, *m;
+    apr_size_t cand_n, n;
+    int i;
+    
+    candidate = md_get_by_name(mds, md->name);
+    if (!candidate) {
+        /* try to find an instance that contains all domain names from md */ 
+        for (i = 0; i < mds->nelts; ++i) {
+            m = APR_ARRAY_IDX(mds, i, md_t *);
+            if (md_contains_domains(m, md)) {
+                return m;
+            }
+        }
+        /* no matching name and no md in the list has all domains.
+         * We consider that managed domain as closest match that contains at least one
+         * domain name from md, ONLY if there is no other one that also has.
+         */
+        cand_n = 0;
+        for (i = 0; i < mds->nelts; ++i) {
+            m = APR_ARRAY_IDX(mds, i, md_t *);
+            n = md_common_name_count(md, m);
+            if (n > cand_n) {
+                candidate = m;
+                cand_n = n;
+            }
+        }
+    }
+    return candidate;
+}
+
+md_t *md_get_by_name(struct apr_array_header_t *mds, const char *name)
+{
+    int i;
+    for (i = 0; i < mds->nelts; ++i) {
+        md_t *md = APR_ARRAY_IDX(mds, i, md_t *);
+        if (!strcmp(name, md->name)) {
+            return md;
+        }
+    }
+    return NULL;
+}
+
+md_t *md_get_by_domain(struct apr_array_header_t *mds, const char *domain)
+{
+    int i;
+    for (i = 0; i < mds->nelts; ++i) {
+        md_t *md = APR_ARRAY_IDX(mds, i, md_t *);
+        if (md_contains(md, domain, 0)) {
+            return md;
+        }
+    }
+    return NULL;
+}
+
+md_t *md_get_by_dns_overlap(struct apr_array_header_t *mds, const md_t *md)
+{
+    int i;
+    for (i = 0; i < mds->nelts; ++i) {
+        md_t *o = APR_ARRAY_IDX(mds, i, md_t *);
+        if (strcmp(o->name, md->name) && md_common_name(o, md)) {
+            return o;
+        }
+    }
+    return NULL;
+}
+
+md_t *md_create(apr_pool_t *p, apr_array_header_t *domains)
+{
+    md_t *md;
+    
+    md = md_create_empty(p);
+    md->domains = md_array_str_compact(p, domains, 0);
+    md->name = APR_ARRAY_IDX(md->domains, 0, const char *);
+    
+    return md;
+}
+
+int md_should_renew(const md_t *md) 
+{
+    apr_time_t now = apr_time_now();
+
+    if (md->expires <= now) {
+        return 1;
+    }
+    else if (md->expires > 0) {
+        double renew_win,  life;
+        apr_interval_time_t left;
+        
+        renew_win = (double)md->renew_window;
+        if (md->renew_norm > 0 
+            && md->renew_norm > renew_win
+            && md->expires > md->valid_from) {
+            /* Calc renewal days as fraction of cert lifetime - if known */
+            life = (double)(md->expires - md->valid_from); 
+            renew_win = life * renew_win / (double)md->renew_norm;
+        }
+        
+        left = md->expires - now;
+        if (left <= renew_win) {
+            return 1;
+        }                
+    }
+    return 0;
+}
+
+/**************************************************************************************************/
+/* lifetime */
+
+md_t *md_copy(apr_pool_t *p, const md_t *src)
+{
+    md_t *md;
+    
+    md = apr_pcalloc(p, sizeof(*md));
+    if (md) {
+        memcpy(md, src, sizeof(*md));
+        md->domains = apr_array_copy(p, src->domains);
+        md->contacts = apr_array_copy(p, src->contacts);
+        if (src->ca_challenges) {
+            md->ca_challenges = apr_array_copy(p, src->ca_challenges);
+        }
+    }    
+    return md;   
+}
+
+md_t *md_clone(apr_pool_t *p, const md_t *src)
+{
+    md_t *md;
+    
+    md = apr_pcalloc(p, sizeof(*md));
+    if (md) {
+        md->state = src->state;
+        md->name = apr_pstrdup(p, src->name);
+        md->require_https = src->require_https;
+        md->must_staple = src->must_staple;
+        md->drive_mode = src->drive_mode;
+        md->domains = md_array_str_compact(p, src->domains, 0);
+        md->pkey_spec = src->pkey_spec;
+        md->renew_norm = src->renew_norm;
+        md->renew_window = src->renew_window;
+        md->contacts = md_array_str_clone(p, src->contacts);
+        if (src->ca_url) md->ca_url = apr_pstrdup(p, src->ca_url);
+        if (src->ca_proto) md->ca_proto = apr_pstrdup(p, src->ca_proto);
+        if (src->ca_account) md->ca_account = apr_pstrdup(p, src->ca_account);
+        if (src->ca_agreement) md->ca_agreement = apr_pstrdup(p, src->ca_agreement);
+        if (src->defn_name) md->defn_name = apr_pstrdup(p, src->defn_name);
+        if (src->cert_url) md->cert_url = apr_pstrdup(p, src->cert_url);
+        md->defn_line_number = src->defn_line_number;
+        if (src->ca_challenges) {
+            md->ca_challenges = md_array_str_clone(p, src->ca_challenges);
+        }
+    }    
+    return md;   
+}
+
+md_t *md_merge(apr_pool_t *p, const md_t *add, const md_t *base)
+{
+    md_t *n = apr_pcalloc(p, sizeof(*n));
+
+    n->ca_url = add->ca_url? add->ca_url : base->ca_url;
+    n->ca_proto = add->ca_proto? add->ca_proto : base->ca_proto;
+    n->ca_agreement = add->ca_agreement? add->ca_agreement : base->ca_agreement;
+    n->require_https = (add->require_https != MD_REQUIRE_UNSET)? add->require_https : base->require_https;
+    n->must_staple = (add->must_staple >= 0)? add->must_staple : base->must_staple;
+    n->drive_mode = (add->drive_mode != MD_DRIVE_DEFAULT)? add->drive_mode : base->drive_mode;
+    n->pkey_spec = add->pkey_spec? add->pkey_spec : base->pkey_spec;
+    n->renew_norm = (add->renew_norm > 0)? add->renew_norm : base->renew_norm;
+    n->renew_window = (add->renew_window > 0)? add->renew_window : base->renew_window;
+    n->transitive = (add->transitive >= 0)? add->transitive : base->transitive;
+    if (add->ca_challenges) {
+        n->ca_challenges = apr_array_copy(p, add->ca_challenges);
+    }
+    else if (base->ca_challenges) {
+        n->ca_challenges = apr_array_copy(p, base->ca_challenges);
+    }
+    return n;
+}
+
+/**************************************************************************************************/
+/* format conversion */
+
+md_json_t *md_to_json(const md_t *md, apr_pool_t *p)
+{
+    md_json_t *json = md_json_create(p);
+    if (json) {
+        apr_array_header_t *domains = md_array_str_compact(p, md->domains, 0);
+        md_json_sets(md->name, json, MD_KEY_NAME, NULL);
+        md_json_setsa(domains, json, MD_KEY_DOMAINS, NULL);
+        md_json_setsa(md->contacts, json, MD_KEY_CONTACTS, NULL);
+        md_json_setl(md->transitive, json, MD_KEY_TRANSITIVE, NULL);
+        md_json_sets(md->ca_account, json, MD_KEY_CA, MD_KEY_ACCOUNT, NULL);
+        md_json_sets(md->ca_proto, json, MD_KEY_CA, MD_KEY_PROTO, NULL);
+        md_json_sets(md->ca_url, json, MD_KEY_CA, MD_KEY_URL, NULL);
+        md_json_sets(md->ca_agreement, json, MD_KEY_CA, MD_KEY_AGREEMENT, NULL);
+        if (md->cert_url) {
+            md_json_sets(md->cert_url, json, MD_KEY_CERT, MD_KEY_URL, NULL);
+        }
+        if (md->pkey_spec) {
+            md_json_setj(md_pkey_spec_to_json(md->pkey_spec, p), json, MD_KEY_PKEY, NULL);
+        }
+        md_json_setl(md->state, json, MD_KEY_STATE, NULL);
+        md_json_setl(md->drive_mode, json, MD_KEY_DRIVE_MODE, NULL);
+        if (md->expires > 0) {
+            char *ts = apr_pcalloc(p, APR_RFC822_DATE_LEN);
+            apr_rfc822_date(ts, md->expires);
+            md_json_sets(ts, json, MD_KEY_CERT, MD_KEY_EXPIRES, NULL);
+        }
+        if (md->valid_from > 0) {
+            char *ts = apr_pcalloc(p, APR_RFC822_DATE_LEN);
+            apr_rfc822_date(ts, md->valid_from);
+            md_json_sets(ts, json, MD_KEY_CERT, MD_KEY_VALID_FROM, NULL);
+        }
+        if (md->renew_norm > 0) {
+            md_json_sets(apr_psprintf(p, "%ld%%", (long)(md->renew_window * 100L / md->renew_norm)), 
+                                      json, MD_KEY_RENEW_WINDOW, NULL);
+        }
+        else {
+            md_json_setl((long)apr_time_sec(md->renew_window), json, MD_KEY_RENEW_WINDOW, NULL);
+        }
+        md_json_setb(md_should_renew(md), json, MD_KEY_RENEW, NULL);
+        if (md->ca_challenges && md->ca_challenges->nelts > 0) {
+            apr_array_header_t *na;
+            na = md_array_str_compact(p, md->ca_challenges, 0);
+            md_json_setsa(na, json, MD_KEY_CA, MD_KEY_CHALLENGES, NULL);
+        }
+        switch (md->require_https) {
+            case MD_REQUIRE_TEMPORARY:
+                md_json_sets(MD_KEY_TEMPORARY, json, MD_KEY_REQUIRE_HTTPS, NULL);
+                break;
+            case MD_REQUIRE_PERMANENT:
+                md_json_sets(MD_KEY_PERMANENT, json, MD_KEY_REQUIRE_HTTPS, NULL);
+                break;
+            default:
+                break;
+        }
+        md_json_setb(md->must_staple > 0, json, MD_KEY_MUST_STAPLE, NULL);
+        return json;
+    }
+    return NULL;
+}
+
+md_t *md_from_json(md_json_t *json, apr_pool_t *p)
+{
+    const char *s;
+    md_t *md = md_create_empty(p);
+    if (md) {
+        md->name = md_json_dups(p, json, MD_KEY_NAME, NULL);            
+        md_json_dupsa(md->domains, p, json, MD_KEY_DOMAINS, NULL);
+        md_json_dupsa(md->contacts, p, json, MD_KEY_CONTACTS, NULL);
+        md->ca_account = md_json_dups(p, json, MD_KEY_CA, MD_KEY_ACCOUNT, NULL);
+        md->ca_proto = md_json_dups(p, json, MD_KEY_CA, MD_KEY_PROTO, NULL);
+        md->ca_url = md_json_dups(p, json, MD_KEY_CA, MD_KEY_URL, NULL);
+        md->ca_agreement = md_json_dups(p, json, MD_KEY_CA, MD_KEY_AGREEMENT, NULL);
+        md->cert_url = md_json_dups(p, json, MD_KEY_CERT, MD_KEY_URL, NULL);
+        if (md_json_has_key(json, MD_KEY_PKEY, MD_KEY_TYPE, NULL)) {
+            md->pkey_spec = md_pkey_spec_from_json(md_json_getj(json, MD_KEY_PKEY, NULL), p);
+        }
+        md->state = (md_state_t)md_json_getl(json, MD_KEY_STATE, NULL);
+        md->drive_mode = (int)md_json_getl(json, MD_KEY_DRIVE_MODE, NULL);
+        md->domains = md_array_str_compact(p, md->domains, 0);
+        md->transitive = (int)md_json_getl(json, MD_KEY_TRANSITIVE, NULL);
+        s = md_json_dups(p, json, MD_KEY_CERT, MD_KEY_EXPIRES, NULL);
+        if (s && *s) {
+            md->expires = apr_date_parse_rfc(s);
+        }
+        s = md_json_dups(p, json, MD_KEY_CERT, MD_KEY_VALID_FROM, NULL);
+        if (s && *s) {
+            md->valid_from = apr_date_parse_rfc(s);
+        }
+        md->renew_norm = 0;
+        md->renew_window = apr_time_from_sec(md_json_getl(json, MD_KEY_RENEW_WINDOW, NULL));
+        if (md->renew_window <= 0) {
+            s = md_json_gets(json, MD_KEY_RENEW_WINDOW, NULL);
+            if (s && strchr(s, '%')) {
+                int percent = atoi(s);
+                if (0 < percent && percent < 100) {
+                    md->renew_norm = apr_time_from_sec(100 * MD_SECS_PER_DAY);
+                    md->renew_window = apr_time_from_sec(percent * MD_SECS_PER_DAY);
+                }
+            }
+        }
+        if (md_json_has_key(json, MD_KEY_CA, MD_KEY_CHALLENGES, NULL)) {
+            md->ca_challenges = apr_array_make(p, 5, sizeof(const char*));
+            md_json_dupsa(md->ca_challenges, p, json, MD_KEY_CA, MD_KEY_CHALLENGES, NULL);
+        }
+        md->require_https = MD_REQUIRE_OFF;
+        s = md_json_gets(json, MD_KEY_REQUIRE_HTTPS, NULL);
+        if (s && !strcmp(MD_KEY_TEMPORARY, s)) {
+            md->require_https = MD_REQUIRE_TEMPORARY;
+        }
+        else if (s && !strcmp(MD_KEY_PERMANENT, s)) {
+            md->require_https = MD_REQUIRE_PERMANENT;
+        }
+        md->must_staple = (int)md_json_getb(json, MD_KEY_MUST_STAPLE, NULL);
+        
+        return md;
+    }
+    return NULL;
+}
+
diff --git a/modules/md/md_crypt.c b/modules/md/md_crypt.c
new file mode 100644 (file)
index 0000000..2a6dd24
--- /dev/null
@@ -0,0 +1,1289 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_buckets.h>
+#include <apr_file_io.h>
+#include <apr_strings.h>
+
+#include <openssl/err.h>
+#include <openssl/evp.h>
+#include <openssl/pem.h>
+#include <openssl/rand.h>
+#include <openssl/rsa.h>
+#include <openssl/x509v3.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_log.h"
+#include "md_http.h"
+#include "md_util.h"
+
+/* getpid for *NIX */
+#if APR_HAVE_SYS_TYPES_H
+#include <sys/types.h>
+#endif
+#if APR_HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+
+/* getpid for Windows */
+#if APR_HAVE_PROCESS_H
+#include <process.h>
+#endif
+
+static int initialized;
+
+struct md_pkey_t {
+    apr_pool_t *pool;
+    EVP_PKEY   *pkey;
+};
+
+#ifdef MD_HAVE_ARC4RANDOM
+
+static void seed_RAND(int pid)
+{
+    char seed[128];
+    
+    (void)pid;
+    arc4random_buf(seed, sizeof(seed));
+    RAND_seed(seed, sizeof(seed));
+}
+
+#else /* ifdef MD_HAVE_ARC4RANDOM */
+
+static int rand_choosenum(int l, int h)
+{
+    int i;
+    char buf[50];
+
+    apr_snprintf(buf, sizeof(buf), "%.0f",
+                 (((double)(rand()%RAND_MAX)/RAND_MAX)*(h-l)));
+    i = atoi(buf)+1;
+    if (i < l) i = l;
+    if (i > h) i = h;
+    return i;
+}
+
+static void seed_RAND(int pid)
+{   
+    unsigned char stackdata[256];
+    /* stolen from mod_ssl/ssl_engine_rand.c */
+    int n;
+    struct {
+        time_t t;
+        pid_t pid;
+    } my_seed;
+    
+    /*
+     * seed in the current time (usually just 4 bytes)
+     */
+    my_seed.t = time(NULL);
+    
+    /*
+     * seed in the current process id (usually just 4 bytes)
+     */
+    my_seed.pid = pid;
+    
+    RAND_seed((unsigned char *)&my_seed, sizeof(my_seed));
+    
+    /*
+     * seed in some current state of the run-time stack (128 bytes)
+     */
+    n = rand_choosenum(0, sizeof(stackdata)-128-1);
+    RAND_seed(stackdata+n, 128);
+}
+
+#endif /*ifdef MD_HAVE_ARC4RANDOM (else part) */
+
+
+apr_status_t md_crypt_init(apr_pool_t *pool)
+{
+    (void)pool;
+    
+    if (!initialized) {
+        int pid = getpid();
+        
+        ERR_load_crypto_strings();
+        OpenSSL_add_all_algorithms();
+        
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, 0, pool, "initializing RAND"); 
+        while (!RAND_status()) {
+            seed_RAND(pid);
+       }
+
+        initialized = 1;
+    }
+    return APR_SUCCESS;
+}
+
+typedef struct {
+    char *data;
+    apr_size_t len;
+} buffer_rec;
+
+static apr_status_t fwrite_buffer(void *baton, apr_file_t *f, apr_pool_t *p) 
+{
+    buffer_rec *buf = baton;
+    
+    (void)p;
+    return apr_file_write_full(f, buf->data, buf->len, &buf->len);
+}
+
+apr_status_t md_rand_bytes(unsigned char *buf, apr_size_t len, apr_pool_t *p)
+{
+    apr_status_t rv;
+    
+    if (len > INT_MAX) {
+        return APR_ENOTIMPL;
+    }
+    if (APR_SUCCESS == (rv = md_crypt_init(p))) {
+        RAND_bytes((unsigned char*)buf, (int)len);
+    }
+    return rv;
+}
+
+typedef struct {
+    const char *pass_phrase;
+    int pass_len;
+} passwd_ctx;
+
+static int pem_passwd(char *buf, int size, int rwflag, void *baton)
+{
+    passwd_ctx *ctx = baton;
+    
+    (void)rwflag;
+    if (ctx->pass_len > 0) {
+        if (ctx->pass_len < size) {
+            size = (int)ctx->pass_len;
+        }
+        memcpy(buf, ctx->pass_phrase, (size_t)size);
+    }
+    return ctx->pass_len;
+}
+
+/**************************************************************************************************/
+/* date time things */
+
+/* Get the apr time (micro seconds, since 1970) from an ASN1 time, as stored in X509
+ * certificates. OpenSSL now has a utility function, but other *SSL derivatives have
+ * not caughts up yet or chose to ignore. An alternative is implemented, we prefer 
+ * however the *SSL to maintain such things.
+ */
+static apr_time_t md_asn1_time_get(const ASN1_TIME* time)
+{
+#ifdef LIBRESSL_VERSION_NUMBER
+    /* courtesy: https://stackoverflow.com/questions/10975542/asn1-time-to-time-t-conversion#11263731
+     * all bugs are mine */
+    apr_time_exp_t t;
+    apr_time_t ts;
+    const char* str = (const char*) time->data;
+    apr_size_t i = 0;
+
+    memset(&t, 0, sizeof(t));
+
+    if (time->type == V_ASN1_UTCTIME) {/* two digit year */
+        t.tm_year = (str[i++] - '0') * 10;
+        t.tm_year += (str[i++] - '0');
+        if (t.tm_year < 70)
+            t.tm_year += 100;
+    } 
+    else if (time->type == V_ASN1_GENERALIZEDTIME) {/* four digit year */
+        t.tm_year = (str[i++] - '0') * 1000;
+        t.tm_year+= (str[i++] - '0') * 100;
+        t.tm_year+= (str[i++] - '0') * 10;
+        t.tm_year+= (str[i++] - '0');
+        t.tm_year -= 1900;
+    }
+    t.tm_mon  = (str[i++] - '0') * 10;
+    t.tm_mon += (str[i++] - '0') - 1; /* -1 since January is 0 not 1. */
+    t.tm_mday = (str[i++] - '0') * 10;
+    t.tm_mday+= (str[i++] - '0');
+    t.tm_hour = (str[i++] - '0') * 10;
+    t.tm_hour+= (str[i++] - '0');
+    t.tm_min  = (str[i++] - '0') * 10;
+    t.tm_min += (str[i++] - '0');
+    t.tm_sec  = (str[i++] - '0') * 10;
+    t.tm_sec += (str[i++] - '0');
+    
+    if (APR_SUCCESS == apr_time_exp_gmt_get(&ts, &t)) {
+        return ts;
+    }
+    return 0;
+#else 
+    int secs, days;
+    apr_time_t ts = apr_time_now();
+    
+    if (ASN1_TIME_diff(&days, &secs, NULL, time)) {
+        ts += apr_time_from_sec((days * MD_SECS_PER_DAY) + secs); 
+    }
+    return ts;
+#endif
+}
+
+
+/**************************************************************************************************/
+/* private keys */
+
+md_json_t *md_pkey_spec_to_json(const md_pkey_spec_t *spec, apr_pool_t *p)
+{
+    md_json_t *json = md_json_create(p);
+    if (json) {
+        switch (spec->type) {
+            case MD_PKEY_TYPE_DEFAULT:
+                md_json_sets("Default", json, MD_KEY_TYPE, NULL);
+                break;
+            case MD_PKEY_TYPE_RSA:
+                md_json_sets("RSA", json, MD_KEY_TYPE, NULL);
+                if (spec->params.rsa.bits >= MD_PKEY_RSA_BITS_MIN) {
+                    md_json_setl((long)spec->params.rsa.bits, json, MD_KEY_BITS, NULL);
+                }
+                break;
+            default:
+                md_json_sets("Unsupported", json, MD_KEY_TYPE, NULL);
+                break;
+        }
+    }
+    return json;    
+}
+
+md_pkey_spec_t *md_pkey_spec_from_json(struct md_json_t *json, apr_pool_t *p)
+{
+    md_pkey_spec_t *spec = apr_pcalloc(p, sizeof(*spec));
+    const char *s;
+    long l;
+    
+    if (spec) {
+        s = md_json_gets(json, MD_KEY_TYPE, NULL);
+        if (!s || !apr_strnatcasecmp("Default", s)) {
+            spec->type = MD_PKEY_TYPE_DEFAULT;
+        }
+        else if (!apr_strnatcasecmp("RSA", s)) {
+            spec->type = MD_PKEY_TYPE_RSA;
+            l = md_json_getl(json, MD_KEY_BITS, NULL);
+            if (l >= MD_PKEY_RSA_BITS_MIN) {
+                spec->params.rsa.bits = (unsigned int)l;
+            }
+            else {
+                spec->params.rsa.bits = MD_PKEY_RSA_BITS_DEF;
+            }
+        }
+    }
+    return spec;
+}
+
+int md_pkey_spec_eq(md_pkey_spec_t *spec1, md_pkey_spec_t *spec2)
+{
+    if (spec1 == spec2) {
+        return 1;
+    }
+    if (spec1 && spec2 && spec1->type == spec2->type) {
+        switch (spec1->type) {
+            case MD_PKEY_TYPE_DEFAULT:
+                return 1;
+            case MD_PKEY_TYPE_RSA:
+                if (spec1->params.rsa.bits == spec2->params.rsa.bits) {
+                    return 1;
+                }
+                break;
+        }
+    }
+    return 0;
+}
+
+static md_pkey_t *make_pkey(apr_pool_t *p) 
+{
+    md_pkey_t *pkey = apr_pcalloc(p, sizeof(*pkey));
+    pkey->pool = p;
+    return pkey;
+}
+
+static apr_status_t pkey_cleanup(void *data)
+{
+    md_pkey_t *pkey = data;
+    if (pkey->pkey) {
+        EVP_PKEY_free(pkey->pkey);
+        pkey->pkey = NULL;
+    }
+    return APR_SUCCESS;
+}
+
+void md_pkey_free(md_pkey_t *pkey)
+{
+    pkey_cleanup(pkey);
+}
+
+void *md_pkey_get_EVP_PKEY(struct md_pkey_t *pkey)
+{
+    return pkey->pkey;
+}
+
+apr_status_t md_pkey_fload(md_pkey_t **ppkey, apr_pool_t *p, 
+                           const char *key, apr_size_t key_len,
+                           const char *fname)
+{
+    apr_status_t rv = APR_ENOENT;
+    md_pkey_t *pkey;
+    BIO *bf;
+    passwd_ctx ctx;
+    
+    pkey =  make_pkey(p);
+    if (NULL != (bf = BIO_new_file(fname, "r"))) {
+        ctx.pass_phrase = key;
+        ctx.pass_len = (int)key_len;
+        
+        ERR_clear_error();
+        pkey->pkey = PEM_read_bio_PrivateKey(bf, NULL, pem_passwd, &ctx);
+        BIO_free(bf);
+        
+        if (pkey->pkey != NULL) {
+            rv = APR_SUCCESS;
+            apr_pool_cleanup_register(p, pkey, pkey_cleanup, apr_pool_cleanup_null);
+        }
+        else {
+            unsigned long err = ERR_get_error();
+            rv = APR_EINVAL;
+            md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, 
+                          "error loading pkey %s: %s (pass phrase was %snull)", fname,
+                          ERR_error_string(err, NULL), key? "not " : ""); 
+        }
+    }
+    *ppkey = (APR_SUCCESS == rv)? pkey : NULL;
+    return rv;
+}
+
+static apr_status_t pkey_to_buffer(buffer_rec *buffer, md_pkey_t *pkey, apr_pool_t *p,
+                                   const char *pass, apr_size_t pass_len)
+{
+    BIO *bio = BIO_new(BIO_s_mem());
+    const EVP_CIPHER *cipher = NULL;
+    pem_password_cb *cb = NULL;
+    void *cb_baton = NULL;
+    passwd_ctx ctx;
+    unsigned long err;
+    int i;
+    
+    if (!bio) {
+        return APR_ENOMEM;
+    }
+    if (pass_len > INT_MAX) {
+        return APR_EINVAL;
+    }
+    if (pass && pass_len > 0) {
+        ctx.pass_phrase = pass;
+        ctx.pass_len = (int)pass_len;
+        cb = pem_passwd;
+        cb_baton = &ctx;
+        cipher = EVP_aes_256_cbc();
+        if (!cipher) {
+            return APR_ENOTIMPL;
+        }
+    }
+    
+    ERR_clear_error();
+    if (!PEM_write_bio_PrivateKey(bio, pkey->pkey, cipher, NULL, 0, cb, cb_baton)) {
+        BIO_free(bio);
+        err = ERR_get_error();
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "PEM_write key: %ld %s", 
+                      err, ERR_error_string(err, NULL)); 
+        return APR_EINVAL;
+    }
+
+    i = BIO_pending(bio);
+    if (i > 0) {
+        buffer->data = apr_palloc(p, (apr_size_t)i + 1);
+        i = BIO_read(bio, buffer->data, i);
+        buffer->data[i] = '\0';
+        buffer->len = (apr_size_t)i;
+    }
+    BIO_free(bio);
+    return APR_SUCCESS;
+}
+
+apr_status_t md_pkey_fsave(md_pkey_t *pkey, apr_pool_t *p, 
+                           const char *pass_phrase, apr_size_t pass_len,
+                           const char *fname, apr_fileperms_t perms)
+{
+    buffer_rec buffer;
+    apr_status_t rv;
+    
+    if (APR_SUCCESS == (rv = pkey_to_buffer(&buffer, pkey, p, pass_phrase, pass_len))) {
+        return md_util_freplace(fname, perms, p, fwrite_buffer, &buffer); 
+    }
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "save pkey %s (%s pass phrase, len=%d)",
+                  fname, pass_len > 0? "with" : "without", (int)pass_len); 
+    return rv;
+}
+
+static apr_status_t gen_rsa(md_pkey_t **ppkey, apr_pool_t *p, unsigned int bits)
+{
+    EVP_PKEY_CTX *ctx = NULL;
+    apr_status_t rv;
+    
+    *ppkey = make_pkey(p);
+    ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
+    if (ctx 
+        && EVP_PKEY_keygen_init(ctx) >= 0
+        && EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, (int)bits) >= 0
+        && EVP_PKEY_keygen(ctx, &(*ppkey)->pkey) >= 0) {
+        rv = APR_SUCCESS;
+    }
+    else {
+        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, "error generate pkey RSA %d", bits); 
+        *ppkey = NULL;
+        rv = APR_EGENERAL;
+    }
+    
+    if (ctx != NULL) {
+        EVP_PKEY_CTX_free(ctx);
+    }
+    return rv;
+}
+
+apr_status_t md_pkey_gen(md_pkey_t **ppkey, apr_pool_t *p, md_pkey_spec_t *spec)
+{
+    md_pkey_type_t ptype = spec? spec->type : MD_PKEY_TYPE_DEFAULT;
+    switch (ptype) {
+        case MD_PKEY_TYPE_DEFAULT:
+            return gen_rsa(ppkey, p, MD_PKEY_RSA_BITS_DEF);
+        case MD_PKEY_TYPE_RSA:
+            return gen_rsa(ppkey, p, spec->params.rsa.bits);
+        default:
+            return APR_ENOTIMPL;
+    }
+}
+
+#if OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER)
+
+#ifndef NID_tlsfeature
+#define NID_tlsfeature          1020
+#endif
+
+static void RSA_get0_key(const RSA *r,
+                         const BIGNUM **n, const BIGNUM **e, const BIGNUM **d)
+{
+    if (n != NULL)
+        *n = r->n;
+    if (e != NULL)
+        *e = r->e;
+    if (d != NULL)
+        *d = r->d;
+}
+
+#endif
+
+static const char *bn64(const BIGNUM *b, apr_pool_t *p) 
+{
+    if (b) {
+         apr_size_t len = (apr_size_t)BN_num_bytes(b);
+         char *buffer = apr_pcalloc(p, len);
+         if (buffer) {
+            BN_bn2bin(b, (unsigned char *)buffer);
+            return md_util_base64url_encode(buffer, len, p);
+         }
+    }
+    return NULL;
+}
+
+const char *md_pkey_get_rsa_e64(md_pkey_t *pkey, apr_pool_t *p)
+{
+    const BIGNUM *e;
+    RSA *rsa = EVP_PKEY_get1_RSA(pkey->pkey);
+    
+    if (!rsa) {
+        return NULL;
+    }
+    RSA_get0_key(rsa, NULL, &e, NULL);
+    return bn64(e, p);
+}
+
+const char *md_pkey_get_rsa_n64(md_pkey_t *pkey, apr_pool_t *p)
+{
+    const BIGNUM *n;
+    RSA *rsa = EVP_PKEY_get1_RSA(pkey->pkey);
+    
+    if (!rsa) {
+        return NULL;
+    }
+    RSA_get0_key(rsa, &n, NULL, NULL);
+    return bn64(n, p);
+}
+
+apr_status_t md_crypt_sign64(const char **psign64, md_pkey_t *pkey, apr_pool_t *p, 
+                             const char *d, size_t dlen)
+{
+    EVP_MD_CTX *ctx = NULL;
+    char *buffer;
+    unsigned int blen;
+    const char *sign64 = NULL;
+    apr_status_t rv = APR_ENOMEM;
+    
+    buffer = apr_pcalloc(p, (apr_size_t)EVP_PKEY_size(pkey->pkey));
+    if (buffer) {
+        ctx = EVP_MD_CTX_create();
+        if (ctx) {
+            rv = APR_ENOTIMPL;
+            if (EVP_SignInit_ex(ctx, EVP_sha256(), NULL)) {
+                rv = APR_EGENERAL;
+                if (EVP_SignUpdate(ctx, d, dlen)) {
+                    if (EVP_SignFinal(ctx, (unsigned char*)buffer, &blen, pkey->pkey)) {
+                        sign64 = md_util_base64url_encode(buffer, blen, p);
+                        if (sign64) {
+                            rv = APR_SUCCESS;
+                        }
+                    }
+                }
+            }
+        }
+        
+        if (ctx) {
+            EVP_MD_CTX_destroy(ctx);
+        }
+    }
+    
+    if (rv != APR_SUCCESS) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "signing"); 
+    }
+    
+    *psign64 = sign64;
+    return rv;
+}
+
+static apr_status_t sha256_digest(unsigned char **pdigest, size_t *pdigest_len,
+                                  apr_pool_t *p, const char *d, size_t dlen)
+{
+    EVP_MD_CTX *ctx = NULL;
+    unsigned char *buffer;
+    apr_status_t rv = APR_ENOMEM;
+    unsigned int blen;
+    
+    buffer = apr_pcalloc(p, EVP_MAX_MD_SIZE);
+    if (buffer) {
+        ctx = EVP_MD_CTX_create();
+        if (ctx) {
+            rv = APR_ENOTIMPL;
+            if (EVP_DigestInit_ex(ctx, EVP_sha256(), NULL)) {
+                rv = APR_EGENERAL;
+                if (EVP_DigestUpdate(ctx, d, dlen)) {
+                    if (EVP_DigestFinal(ctx, buffer, &blen)) {
+                        rv = APR_SUCCESS;
+                    }
+                }
+            }
+        }
+        
+        if (ctx) {
+            EVP_MD_CTX_destroy(ctx);
+        }
+    }
+    
+    if (APR_SUCCESS == rv) {
+        *pdigest = buffer;
+        *pdigest_len = blen;
+    }
+    else {
+        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "digest"); 
+        *pdigest = NULL;
+        *pdigest_len = 0;
+    }
+    return rv;
+}
+
+apr_status_t md_crypt_sha256_digest64(const char **pdigest64, apr_pool_t *p, 
+                                      const char *d, size_t dlen)
+{
+    const char *digest64 = NULL;
+    unsigned char *buffer;
+    size_t blen;
+    apr_status_t rv;
+    
+    if (APR_SUCCESS == (rv = sha256_digest(&buffer, &blen, p, d, dlen))) {
+        if (NULL == (digest64 = md_util_base64url_encode((const char*)buffer, blen, p))) {
+            rv = APR_EGENERAL;
+        }
+    }
+    *pdigest64 = digest64;
+    return rv;
+}
+
+static const char * const hex_const[] = {
+    "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0a", "0b", "0c", "0d", "0e", "0f", 
+    "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1a", "1b", "1c", "1d", "1e", "1f", 
+    "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2a", "2b", "2c", "2d", "2e", "2f", 
+    "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3a", "3b", "3c", "3d", "3e", "3f", 
+    "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4a", "4b", "4c", "4d", "4e", "4f", 
+    "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5a", "5b", "5c", "5d", "5e", "5f", 
+    "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6a", "6b", "6c", "6d", "6e", "6f", 
+    "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7a", "7b", "7c", "7d", "7e", "7f", 
+    "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8a", "8b", "8c", "8d", "8e", "8f", 
+    "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9a", "9b", "9c", "9d", "9e", "9f", 
+    "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "aa", "ab", "ac", "ad", "ae", "af", 
+    "b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "ba", "bb", "bc", "bd", "be", "bf", 
+    "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "ca", "cb", "cc", "cd", "ce", "cf", 
+    "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "da", "db", "dc", "dd", "de", "df", 
+    "e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "ea", "eb", "ec", "ed", "ee", "ef", 
+    "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "fa", "fb", "fc", "fd", "fe", "ff", 
+};
+
+apr_status_t md_crypt_sha256_digest_hex(const char **pdigesthex, apr_pool_t *p, 
+                                        const char *d, size_t dlen)
+{
+    char *dhex = NULL, *cp;
+    const char * x;
+    unsigned char *buffer;
+    size_t blen;
+    apr_status_t rv;
+    unsigned int i;
+    
+    if (APR_SUCCESS == (rv = sha256_digest(&buffer, &blen, p, d, dlen))) {
+        cp = dhex = apr_pcalloc(p,  2 * blen + 1);
+        if (!dhex) {
+            rv = APR_EGENERAL;
+        }
+        for (i = 0; i < blen; ++i, cp += 2) {
+            x = hex_const[buffer[i]];
+            cp[0] = x[0];
+            cp[1] = x[1];
+        }
+    }
+    *pdigesthex = dhex;
+    return rv;
+}
+
+/**************************************************************************************************/
+/* certificates */
+
+struct md_cert_t {
+    apr_pool_t *pool;
+    X509 *x509;
+    apr_array_header_t *alt_names;
+};
+
+static apr_status_t cert_cleanup(void *data)
+{
+    md_cert_t *cert = data;
+    if (cert->x509) {
+        X509_free(cert->x509);
+        cert->x509 = NULL;
+    }
+    return APR_SUCCESS;
+}
+
+static md_cert_t *make_cert(apr_pool_t *p, X509 *x509) 
+{
+    md_cert_t *cert = apr_pcalloc(p, sizeof(*cert));
+    cert->pool = p;
+    cert->x509 = x509;
+    apr_pool_cleanup_register(p, cert, cert_cleanup, apr_pool_cleanup_null);
+    
+    return cert;
+}
+
+void md_cert_free(md_cert_t *cert)
+{
+    cert_cleanup(cert);
+}
+
+void *md_cert_get_X509(struct md_cert_t *cert)
+{
+    return cert->x509;
+}
+
+int md_cert_is_valid_now(const md_cert_t *cert)
+{
+    return ((X509_cmp_current_time(X509_get_notBefore(cert->x509)) < 0)
+            && (X509_cmp_current_time(X509_get_notAfter(cert->x509)) > 0));
+}
+
+int md_cert_has_expired(const md_cert_t *cert)
+{
+    return (X509_cmp_current_time(X509_get_notAfter(cert->x509)) <= 0);
+}
+
+apr_time_t md_cert_get_not_after(md_cert_t *cert)
+{
+    return md_asn1_time_get(X509_get_notAfter(cert->x509));
+}
+
+apr_time_t md_cert_get_not_before(md_cert_t *cert)
+{
+    return md_asn1_time_get(X509_get_notBefore(cert->x509));
+}
+
+int md_cert_covers_domain(md_cert_t *cert, const char *domain_name)
+{
+    if (!cert->alt_names) {
+        md_cert_get_alt_names(&cert->alt_names, cert, cert->pool);
+    }
+    if (cert->alt_names) {
+        return md_array_str_index(cert->alt_names, domain_name, 0, 0) >= 0;
+    }
+    return 0;
+}
+
+int md_cert_covers_md(md_cert_t *cert, const md_t *md)
+{
+    const char *name;
+    int i;
+    
+    if (!cert->alt_names) {
+        md_cert_get_alt_names(&cert->alt_names, cert, cert->pool);
+    }
+    if (cert->alt_names) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, cert->pool, "cert has %d alt names",
+                      cert->alt_names->nelts); 
+        for (i = 0; i < md->domains->nelts; ++i) {
+            name = APR_ARRAY_IDX(md->domains, i, const char *);
+            if (md_array_str_index(cert->alt_names, name, 0, 0) < 0) {
+                md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, cert->pool, 
+                              "md domain %s not covered by cert", name);
+                return 0;
+            }
+        }
+        return 1;
+    }
+    else {
+        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, cert->pool, "cert has NO alt names");
+    }
+    return 0;
+}
+
+apr_status_t md_cert_get_issuers_uri(const char **puri, md_cert_t *cert, apr_pool_t *p)
+{
+    int i, ext_idx, nid = NID_info_access;
+    X509_EXTENSION *ext;
+    X509V3_EXT_METHOD *ext_cls;
+    void *ext_data;
+    const char *uri = NULL;
+    apr_status_t rv = APR_ENOENT;
+    
+    /* Waddle through x509  API history to get someone that may be able
+     * to hand us the issuer url for the cert chain */
+    ext_idx = X509_get_ext_by_NID(cert->x509, nid, -1);
+    ext = (ext_idx >= 0)? X509_get_ext(cert->x509, ext_idx) : NULL;
+    ext_cls = ext? (X509V3_EXT_METHOD*)X509V3_EXT_get(ext) : NULL;
+    if (ext_cls && (ext_data = X509_get_ext_d2i(cert->x509, nid, 0, 0))) {
+        CONF_VALUE *cval;
+        STACK_OF(CONF_VALUE) *ext_vals = ext_cls->i2v(ext_cls, ext_data, 0);
+        
+        for (i = 0; i < sk_CONF_VALUE_num(ext_vals); ++i) {
+            cval = sk_CONF_VALUE_value(ext_vals, i);
+            if (!strcmp("CA Issuers - URI", cval->name)) {
+                uri = apr_pstrdup(p, cval->value);
+                rv = APR_SUCCESS;
+                break;
+            }
+        }
+    } 
+    *puri = (APR_SUCCESS == rv)? uri : NULL;
+    return rv;
+}
+
+apr_status_t md_cert_get_alt_names(apr_array_header_t **pnames, md_cert_t *cert, apr_pool_t *p)
+{
+    apr_array_header_t *names;
+    apr_status_t rv = APR_ENOENT;
+    STACK_OF(GENERAL_NAME) *xalt_names;
+    unsigned char *buf;
+    int i;
+    
+    xalt_names = (GENERAL_NAMES*)X509_get_ext_d2i(cert->x509, NID_subject_alt_name, NULL, NULL);
+    if (xalt_names) {
+        GENERAL_NAME *cval;
+        
+        names = apr_array_make(p, sk_GENERAL_NAME_num(xalt_names), sizeof(char *));
+        for (i = 0; i < sk_GENERAL_NAME_num(xalt_names); ++i) {
+            cval = sk_GENERAL_NAME_value(xalt_names, i);
+            switch (cval->type) {
+                case GEN_DNS:
+                case GEN_URI:
+                case GEN_IPADD:
+                    ASN1_STRING_to_UTF8(&buf, cval->d.ia5);
+                    APR_ARRAY_PUSH(names, const char *) = apr_pstrdup(p, (char*)buf);
+                    OPENSSL_free(buf);
+                    break;
+                default:
+                    break;
+            }
+        }
+        rv = APR_SUCCESS;
+    }
+    *pnames = (APR_SUCCESS == rv)? names : NULL;
+    return rv;
+}
+
+apr_status_t md_cert_fload(md_cert_t **pcert, apr_pool_t *p, const char *fname)
+{
+    FILE *f;
+    apr_status_t rv;
+    md_cert_t *cert;
+    X509 *x509;
+    
+    rv = md_util_fopen(&f, fname, "r");
+    if (rv == APR_SUCCESS) {
+    
+        x509 = PEM_read_X509(f, NULL, NULL, NULL);
+        rv = fclose(f);
+        if (x509 != NULL) {
+            cert =  make_cert(p, x509);
+        }
+        else {
+            rv = APR_EINVAL;
+        }
+    }
+
+    *pcert = (APR_SUCCESS == rv)? cert : NULL;
+    return rv;
+}
+
+static apr_status_t cert_to_buffer(buffer_rec *buffer, md_cert_t *cert, apr_pool_t *p)
+{
+    BIO *bio = BIO_new(BIO_s_mem());
+    int i;
+    
+    if (!bio) {
+        return APR_ENOMEM;
+    }
+
+    ERR_clear_error();
+    PEM_write_bio_X509(bio, cert->x509);
+    if (ERR_get_error() > 0) {
+        BIO_free(bio);
+        return APR_EINVAL;
+    }
+
+    i = BIO_pending(bio);
+    if (i > 0) {
+        buffer->data = apr_palloc(p, (apr_size_t)i + 1);
+        i = BIO_read(bio, buffer->data, i);
+        buffer->data[i] = '\0';
+        buffer->len = (apr_size_t)i;
+    }
+    BIO_free(bio);
+    return APR_SUCCESS;
+}
+
+apr_status_t md_cert_fsave(md_cert_t *cert, apr_pool_t *p, 
+                           const char *fname, apr_fileperms_t perms)
+{
+    buffer_rec buffer;
+    apr_status_t rv;
+    
+    if (APR_SUCCESS == (rv = cert_to_buffer(&buffer, cert, p))) {
+        return md_util_freplace(fname, perms, p, fwrite_buffer, &buffer); 
+    }
+    return rv;
+}
+
+apr_status_t md_cert_to_base64url(const char **ps64, md_cert_t *cert, apr_pool_t *p)
+{
+    buffer_rec buffer;
+    apr_status_t rv;
+    
+    if (APR_SUCCESS == (rv = cert_to_buffer(&buffer, cert, p))) {
+        *ps64 = md_util_base64url_encode(buffer.data, buffer.len, p);
+        return APR_SUCCESS;
+    }
+    *ps64 = NULL;
+    return rv;
+}
+
+apr_status_t md_cert_read_http(md_cert_t **pcert, apr_pool_t *p, 
+                               const md_http_response_t *res)
+{
+    const char *ct;
+    apr_off_t data_len;
+    apr_size_t der_len;
+    apr_status_t rv;
+    
+    ct = apr_table_get(res->headers, "Content-Type");
+    if (!res->body || !ct  || strcmp("application/pkix-cert", ct)) {
+        return APR_ENOENT;
+    }
+    
+    if (APR_SUCCESS == (rv = apr_brigade_length(res->body, 1, &data_len))) {
+        char *der;
+        if (data_len > 1024*1024) { /* certs usually are <2k each */
+            return APR_EINVAL;
+        }
+        if (APR_SUCCESS == (rv = apr_brigade_pflatten(res->body, &der, &der_len, p))) {
+            const unsigned char *bf = (const unsigned char*)der;
+            X509 *x509;
+            
+            if (NULL == (x509 = d2i_X509(NULL, &bf, (long)der_len))) {
+                rv = APR_EINVAL;
+            }
+            else {
+                *pcert = make_cert(p, x509);
+                rv = APR_SUCCESS;
+            }
+        }
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, "cert parsed");
+    }
+    return rv;
+}
+
+md_cert_state_t md_cert_state_get(md_cert_t *cert)
+{
+    if (cert->x509) {
+        return md_cert_is_valid_now(cert)? MD_CERT_VALID : MD_CERT_EXPIRED;
+    }
+    return MD_CERT_UNKNOWN;
+}
+
+apr_status_t md_chain_fappend(struct apr_array_header_t *certs, apr_pool_t *p, const char *fname)
+{
+    FILE *f;
+    apr_status_t rv;
+    X509 *x509;
+    md_cert_t *cert;
+    unsigned long err;
+    
+    rv = md_util_fopen(&f, fname, "r");
+    if (rv == APR_SUCCESS) {
+        ERR_clear_error();
+        while (NULL != (x509 = PEM_read_X509(f, NULL, NULL, NULL))) {
+            cert = make_cert(p, x509);
+            APR_ARRAY_PUSH(certs, md_cert_t *) = cert;
+        }
+        fclose(f);
+        
+        if (0 < (err =  ERR_get_error())
+            && !(ERR_GET_LIB(err) == ERR_LIB_PEM && ERR_GET_REASON(err) == PEM_R_NO_START_LINE)) {
+            /* not the expected one when no more PEM encodings are found */
+            rv = APR_EINVAL;
+            goto out;
+        }
+        
+        if (certs->nelts == 0) {
+            /* Did not find any. This is acceptable unless the file has a certain size
+             * when we no longer accept it as empty chain file. Something seems to be
+             * wrong then. */
+            apr_finfo_t info;
+            if (APR_SUCCESS == apr_stat(&info, fname, APR_FINFO_SIZE, p) && info.size >= 1024) {
+                /* "Too big for a moon." */
+                rv = APR_EINVAL;
+                md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, 
+                              "no certificates in non-empty chain %s", fname);
+                goto out;
+            }
+        }        
+    }
+out:
+    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, p, "read chain file %s, found %d certs", 
+                  fname, certs? certs->nelts : 0);
+    return rv;
+}
+
+apr_status_t md_chain_fload(apr_array_header_t **pcerts, apr_pool_t *p, const char *fname)
+{
+    apr_array_header_t *certs;
+    apr_status_t rv;
+
+    certs = apr_array_make(p, 5, sizeof(md_cert_t *));
+    rv = md_chain_fappend(certs, p, fname);
+    *pcerts = (APR_SUCCESS == rv)? certs : NULL;
+    return rv;
+}
+
+apr_status_t md_chain_fsave(apr_array_header_t *certs, apr_pool_t *p, 
+                            const char *fname, apr_fileperms_t perms)
+{
+    FILE *f;
+    apr_status_t rv;
+    const md_cert_t *cert;
+    unsigned long err = 0;
+    int i;
+    
+    (void)p;
+    rv = md_util_fopen(&f, fname, "w");
+    if (rv == APR_SUCCESS) {
+        apr_file_perms_set(fname, perms);
+        ERR_clear_error();
+        for (i = 0; i < certs->nelts; ++i) {
+            cert = APR_ARRAY_IDX(certs, i, const md_cert_t *);
+            assert(cert->x509);
+            
+            PEM_write_X509(f, cert->x509);
+            
+            if (0 < (err = ERR_get_error())) {
+                break;
+            }
+            
+        }
+        rv = fclose(f);
+        if (err) {
+            rv = APR_EINVAL;
+        }
+    }
+    return rv;
+}
+
+/**************************************************************************************************/
+/* certificate signing requests */
+
+static const char *alt_names(apr_array_header_t *domains, apr_pool_t *p)
+{
+    const char *alts = "", *sep = "", *domain;
+    int i;
+    
+    for (i = 0; i < domains->nelts; ++i) {
+        domain = APR_ARRAY_IDX(domains, i, const char *);
+        alts = apr_psprintf(p, "%s%sDNS:%s", alts, sep, domain);
+        sep = ",";
+    }
+    return alts;
+}
+
+static apr_status_t add_ext(X509 *x, int nid, const char *value, apr_pool_t *p)
+{
+    X509_EXTENSION *ext = NULL;
+    X509V3_CTX ctx;
+    apr_status_t rv;
+
+    X509V3_set_ctx_nodb(&ctx);
+    X509V3_set_ctx(&ctx, x, x, NULL, NULL, 0);
+    if (NULL == (ext = X509V3_EXT_conf_nid(NULL, &ctx, nid, (char*)value))) {
+        return APR_EGENERAL;
+    }
+    
+    ERR_clear_error();
+    rv = X509_add_ext(x, ext, -1)? APR_SUCCESS : APR_EINVAL;
+    if (APR_SUCCESS != rv) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "add_ext nid=%dd value='%s'", 
+                      nid, value); 
+        
+    }
+    X509_EXTENSION_free(ext);
+    return rv;
+}
+
+static apr_status_t sk_add_alt_names(STACK_OF(X509_EXTENSION) *exts,
+                                     apr_array_header_t *domains, apr_pool_t *p)
+{
+    if (domains->nelts > 0) {
+        X509_EXTENSION *x;
+        
+        x = X509V3_EXT_conf_nid(NULL, NULL, NID_subject_alt_name, (char*)alt_names(domains, p));
+        if (NULL == x) {
+            return APR_EGENERAL;
+        }
+        sk_X509_EXTENSION_push(exts, x);
+    }
+    return APR_SUCCESS;
+}
+
+static apr_status_t add_must_staple(STACK_OF(X509_EXTENSION) *exts, const md_t *md, apr_pool_t *p)
+{
+    
+    if (md->must_staple) {
+        X509_EXTENSION *x;
+        int nid;
+        
+        nid = OBJ_create("1.3.6.1.5.5.7.1.24", "OCSPReq", "OCSP Request");
+        if (NID_undef == nid) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, 
+                          "%s: unable to get NID for must-staple", md->name);
+            return APR_EGENERAL;
+        }
+        x = X509V3_EXT_conf_nid(NULL, NULL, nid, (char*)"DER:30:03:02:01:05");
+        if (NULL == x) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, 
+                          "%s: unable to get x509 extension for must-staple", md->name);
+            return APR_EGENERAL;
+        }
+        sk_X509_EXTENSION_push(exts, x);
+    }
+    return APR_SUCCESS;
+}
+
+apr_status_t md_cert_req_create(const char **pcsr_der_64, const md_t *md, 
+                                md_pkey_t *pkey, apr_pool_t *p)
+{
+    const char *s, *csr_der, *csr_der_64 = NULL;
+    const unsigned char *domain;
+    X509_REQ *csr;
+    X509_NAME *n = NULL;
+    STACK_OF(X509_EXTENSION) *exts = NULL;
+    apr_status_t rv;
+    int csr_der_len;
+    
+    assert(md->domains->nelts > 0);
+    
+    if (NULL == (csr = X509_REQ_new()) 
+        || NULL == (exts = sk_X509_EXTENSION_new_null())
+        || NULL == (n = X509_NAME_new())) {
+        rv = APR_ENOMEM;
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: openssl alloc X509 things", md->name);
+        goto out; 
+    }
+
+    /* subject name == first domain */
+    domain = APR_ARRAY_IDX(md->domains, 0, const unsigned char *);
+    if (!X509_NAME_add_entry_by_txt(n, "CN", MBSTRING_ASC, domain, -1, -1, 0)
+        || !X509_REQ_set_subject_name(csr, n)) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: REQ name add entry", md->name);
+        rv = APR_EGENERAL; goto out;
+    }
+    /* collect extensions, such as alt names and must staple */
+    if (APR_SUCCESS != (rv = sk_add_alt_names(exts, md->domains, p))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: collecting alt names", md->name);
+        rv = APR_EGENERAL; goto out;
+    }
+    if (APR_SUCCESS != (rv = add_must_staple(exts, md, p))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: must staple", md->name);
+        rv = APR_EGENERAL; goto out;
+    }
+    /* add extensions to csr */
+    if (sk_X509_EXTENSION_num(exts) > 0 && !X509_REQ_add_extensions(csr, exts)) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: adding exts", md->name);
+        rv = APR_EGENERAL; goto out;
+    }
+    /* add our key */
+    if (!X509_REQ_set_pubkey(csr, pkey->pkey)) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set pkey in csr", md->name);
+        rv = APR_EGENERAL; goto out;
+    }
+    /* sign, der encode and base64url encode */
+    if (!X509_REQ_sign(csr, pkey->pkey, EVP_sha256())) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: sign csr", md->name);
+        rv = APR_EGENERAL; goto out;
+    }
+    if ((csr_der_len = i2d_X509_REQ(csr, NULL)) < 0) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: der length", md->name);
+        rv = APR_EGENERAL; goto out;
+    }
+    s = csr_der = apr_pcalloc(p, (apr_size_t)csr_der_len + 1);
+    if (i2d_X509_REQ(csr, (unsigned char**)&s) != csr_der_len) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: csr der enc", md->name);
+        rv = APR_EGENERAL; goto out;
+    }
+    csr_der_64 = md_util_base64url_encode(csr_der, (apr_size_t)csr_der_len, p);
+    rv = APR_SUCCESS;
+    
+out:
+    if (exts) {
+        sk_X509_EXTENSION_pop_free(exts, X509_EXTENSION_free);
+    }
+    if (csr) {
+        X509_REQ_free(csr);
+    }
+    if (n) {
+        X509_NAME_free(n);
+    }
+    *pcsr_der_64 = (APR_SUCCESS == rv)? csr_der_64 : NULL;
+    return rv;
+}
+
+apr_status_t md_cert_self_sign(md_cert_t **pcert, const char *cn, 
+                               apr_array_header_t *domains, md_pkey_t *pkey,
+                               apr_interval_time_t valid_for, apr_pool_t *p)
+{
+    X509 *x;
+    X509_NAME *n = NULL;
+    md_cert_t *cert = NULL;
+    apr_status_t rv;
+    int days;
+    BIGNUM *big_rnd = NULL;
+    ASN1_INTEGER *asn1_rnd = NULL;
+    unsigned char rnd[20];
+    
+    assert(domains);
+    
+    if (NULL == (x = X509_new()) 
+        || NULL == (n = X509_NAME_new())) {
+        rv = APR_ENOMEM;
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: openssl alloc X509 things", cn);
+        goto out; 
+    }
+    
+    if (APR_SUCCESS != (rv = md_rand_bytes(rnd, sizeof(rnd), p))
+        || !(big_rnd = BN_bin2bn(rnd, sizeof(rnd), NULL))
+        || !(asn1_rnd = BN_to_ASN1_INTEGER(big_rnd, NULL))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: setup random serial", cn);
+        rv = APR_EGENERAL; goto out;
+    }
+     
+    if (1 != X509_set_version(x, 2L)) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: setting x.509v3", cn);
+        rv = APR_EGENERAL; goto out;
+    }
+
+    if (!X509_set_serialNumber(x, asn1_rnd)) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: set serial number", cn);
+        rv = APR_EGENERAL; goto out;
+    }
+    /* set common name and issue */
+    if (!X509_NAME_add_entry_by_txt(n, "CN", MBSTRING_ASC, (const unsigned char*)cn, -1, -1, 0)
+        || !X509_set_subject_name(x, n)
+        || !X509_set_issuer_name(x, n)) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "%s: name add entry", cn);
+        rv = APR_EGENERAL; goto out;
+    }
+    /* cert are unconstrained (but not very trustworthy) */
+    if (APR_SUCCESS != (rv = add_ext(x, NID_basic_constraints, "CA:FALSE, pathlen:0", p))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set basic constraints ext", cn);
+        goto out;
+    }
+    /* add the domain as alt name */
+    if (APR_SUCCESS != (rv = add_ext(x, NID_subject_alt_name, alt_names(domains, p), p))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set alt_name ext", cn);
+        goto out;
+    }
+    /* add our key */
+    if (!X509_set_pubkey(x, pkey->pkey)) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: set pkey in x509", cn);
+        rv = APR_EGENERAL; goto out;
+    }
+    
+    days = (int)((apr_time_sec(valid_for) + MD_SECS_PER_DAY - 1)/ MD_SECS_PER_DAY);
+    if (!X509_set_notBefore(x, ASN1_TIME_set(NULL, time(NULL)))) {
+        rv = APR_EGENERAL; goto out;
+    }
+    if (!X509_set_notAfter(x, ASN1_TIME_adj(NULL, time(NULL), days, 0))) {
+        rv = APR_EGENERAL; goto out;
+    }
+
+    /* sign with same key */
+    if (!X509_sign(x, pkey->pkey, EVP_sha256())) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "%s: sign x509", cn);
+        rv = APR_EGENERAL; goto out;
+    }
+
+    cert = make_cert(p, x);
+    rv = APR_SUCCESS;
+    
+out:
+    if (!cert && x) {
+        X509_free(x);
+    }
+    if (n) {
+        X509_NAME_free(n);
+    }
+    if (big_rnd) {
+        BN_free(big_rnd);
+    }
+    if (asn1_rnd) {
+        ASN1_INTEGER_free(asn1_rnd);
+    }
+    *pcert = (APR_SUCCESS == rv)? cert : NULL;
+    return rv;
+}
+
diff --git a/modules/md/md_crypt.h b/modules/md/md_crypt.h
new file mode 100644 (file)
index 0000000..fc7c2d1
--- /dev/null
@@ -0,0 +1,133 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef mod_md_md_crypt_h
+#define mod_md_md_crypt_h
+
+#include <apr_file_io.h>
+
+struct apr_array_header_t;
+struct md_t;
+struct md_http_response_t;
+struct md_cert_t;
+struct md_pkey_t;
+
+/**************************************************************************************************/
+/* random */
+
+apr_status_t md_rand_bytes(unsigned char *buf, apr_size_t len, apr_pool_t *p);
+
+/**************************************************************************************************/
+/* digests */
+apr_status_t md_crypt_sha256_digest64(const char **pdigest64, apr_pool_t *p, 
+                                      const char *d, size_t dlen);
+apr_status_t md_crypt_sha256_digest_hex(const char **pdigesthex, apr_pool_t *p, 
+                                        const char *d, size_t dlen);
+
+/**************************************************************************************************/
+/* private keys */
+
+typedef struct md_pkey_t md_pkey_t;
+
+typedef enum {
+    MD_PKEY_TYPE_DEFAULT,
+    MD_PKEY_TYPE_RSA,
+} md_pkey_type_t;
+
+typedef struct md_pkey_rsa_spec_t {
+    apr_uint32_t bits;
+} md_pkey_rsa_spec_t;
+
+typedef struct md_pkey_spec_t {
+    md_pkey_type_t type;
+    union {
+        md_pkey_rsa_spec_t rsa;
+    } params;
+} md_pkey_spec_t;
+
+apr_status_t md_crypt_init(apr_pool_t *pool);
+
+apr_status_t md_pkey_gen(md_pkey_t **ppkey, apr_pool_t *p, md_pkey_spec_t *spec);
+void md_pkey_free(md_pkey_t *pkey);
+
+const char *md_pkey_get_rsa_e64(md_pkey_t *pkey, apr_pool_t *p);
+const char *md_pkey_get_rsa_n64(md_pkey_t *pkey, apr_pool_t *p);
+
+apr_status_t md_pkey_fload(md_pkey_t **ppkey, apr_pool_t *p, 
+                           const char *pass_phrase, apr_size_t pass_len,
+                           const char *fname);
+apr_status_t md_pkey_fsave(md_pkey_t *pkey, apr_pool_t *p, 
+                           const char *pass_phrase, apr_size_t pass_len, 
+                           const char *fname, apr_fileperms_t perms);
+
+apr_status_t md_crypt_sign64(const char **psign64, md_pkey_t *pkey, apr_pool_t *p, 
+                             const char *d, size_t dlen);
+
+void *md_cert_get_X509(struct md_cert_t *cert);
+void *md_pkey_get_EVP_PKEY(struct md_pkey_t *pkey);
+
+struct md_json_t *md_pkey_spec_to_json(const md_pkey_spec_t *spec, apr_pool_t *p);
+md_pkey_spec_t *md_pkey_spec_from_json(struct md_json_t *json, apr_pool_t *p);
+int md_pkey_spec_eq(md_pkey_spec_t *spec1, md_pkey_spec_t *spec2);
+
+/**************************************************************************************************/
+/* X509 certificates */
+
+typedef struct md_cert_t md_cert_t;
+
+typedef enum {
+    MD_CERT_UNKNOWN,
+    MD_CERT_VALID,
+    MD_CERT_EXPIRED
+} md_cert_state_t;
+
+void md_cert_free(md_cert_t *cert);
+
+apr_status_t md_cert_fload(md_cert_t **pcert, apr_pool_t *p, const char *fname);
+apr_status_t md_cert_fsave(md_cert_t *cert, apr_pool_t *p, 
+                           const char *fname, apr_fileperms_t perms);
+
+apr_status_t md_cert_read_http(md_cert_t **pcert, apr_pool_t *pool, 
+                               const struct md_http_response_t *res);
+
+md_cert_state_t md_cert_state_get(md_cert_t *cert);
+int md_cert_is_valid_now(const md_cert_t *cert);
+int md_cert_has_expired(const md_cert_t *cert);
+int md_cert_covers_domain(md_cert_t *cert, const char *domain_name);
+int md_cert_covers_md(md_cert_t *cert, const struct md_t *md);
+apr_time_t md_cert_get_not_after(md_cert_t *cert);
+apr_time_t md_cert_get_not_before(md_cert_t *cert);
+
+apr_status_t md_cert_get_issuers_uri(const char **puri, md_cert_t *cert, apr_pool_t *p);
+apr_status_t md_cert_get_alt_names(apr_array_header_t **pnames, md_cert_t *cert, apr_pool_t *p);
+
+apr_status_t md_cert_to_base64url(const char **ps64, md_cert_t *cert, apr_pool_t *p);
+apr_status_t md_cert_from_base64url(md_cert_t **pcert, const char *s64, apr_pool_t *p);
+
+apr_status_t md_chain_fload(struct apr_array_header_t **pcerts, 
+                            apr_pool_t *p, const char *fname);
+apr_status_t md_chain_fsave(struct apr_array_header_t *certs, 
+                            apr_pool_t *p, const char *fname, apr_fileperms_t perms);
+apr_status_t md_chain_fappend(struct apr_array_header_t *certs, 
+                              apr_pool_t *p, const char *fname);
+
+apr_status_t md_cert_req_create(const char **pcsr_der_64, const struct md_t *md, 
+                                md_pkey_t *pkey, apr_pool_t *p);
+
+apr_status_t md_cert_self_sign(md_cert_t **pcert, const char *cn, 
+                               struct apr_array_header_t *domains, md_pkey_t *pkey,
+                               apr_interval_time_t valid_for, apr_pool_t *p);
+
+#endif /* md_crypt_h */
diff --git a/modules/md/md_curl.c b/modules/md/md_curl.c
new file mode 100644 (file)
index 0000000..adcae4f
--- /dev/null
@@ -0,0 +1,306 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+
+#include <curl/curl.h>
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_buckets.h>
+
+#include "md_http.h"
+#include "md_log.h"
+#include "md_curl.h"
+
+/**************************************************************************************************/
+/* md_http curl implementation */
+
+
+static apr_status_t curl_status(int curl_code)
+{
+    switch (curl_code) {
+        case CURLE_OK:                   return APR_SUCCESS;
+        case CURLE_UNSUPPORTED_PROTOCOL: return APR_ENOTIMPL; 
+        case CURLE_NOT_BUILT_IN:         return APR_ENOTIMPL; 
+        case CURLE_URL_MALFORMAT:        return APR_EINVAL;
+        case CURLE_COULDNT_RESOLVE_PROXY:return APR_ECONNREFUSED;
+        case CURLE_COULDNT_RESOLVE_HOST: return APR_ECONNREFUSED;
+        case CURLE_COULDNT_CONNECT:      return APR_ECONNREFUSED;
+        case CURLE_REMOTE_ACCESS_DENIED: return APR_EACCES;
+        case CURLE_OUT_OF_MEMORY:        return APR_ENOMEM;
+        case CURLE_OPERATION_TIMEDOUT:   return APR_TIMEUP;
+        case CURLE_SSL_CONNECT_ERROR:    return APR_ECONNABORTED;
+        case CURLE_AGAIN:                return APR_EAGAIN;
+        default:                         return APR_EGENERAL;
+    }
+}
+
+static size_t req_data_cb(void *data, size_t len, size_t nmemb, void *baton)
+{
+    apr_bucket_brigade *body = baton;
+    size_t blen, read_len = 0, max_len = len * nmemb;
+    const char *bdata;
+    apr_bucket *b;
+    apr_status_t rv;
+    
+    while (body && !APR_BRIGADE_EMPTY(body) && max_len > 0) {
+        b = APR_BRIGADE_FIRST(body);
+        if (APR_BUCKET_IS_METADATA(b)) {
+            if (APR_BUCKET_IS_EOS(b)) {
+                body = NULL;
+            }
+        }
+        else {
+            rv = apr_bucket_read(b, &bdata, &blen, APR_BLOCK_READ);
+            if (rv == APR_SUCCESS) {
+                if (blen > max_len) {
+                    apr_bucket_split(b, max_len);
+                    blen = max_len;
+                }
+                memcpy(data, bdata, blen);
+                read_len += blen;
+                max_len -= blen;
+            }
+            else {
+                body = NULL;
+                if (!APR_STATUS_IS_EOF(rv)) {
+                    /* everything beside EOF is an error */
+                    read_len = CURL_READFUNC_ABORT;
+                }
+            }
+            
+        }
+        apr_bucket_delete(b);
+    }
+    
+    return read_len;
+}
+
+static size_t resp_data_cb(void *data, size_t len, size_t nmemb, void *baton)
+{
+    md_http_response_t *res = baton;
+    size_t blen = len * nmemb;
+    apr_status_t rv;
+    
+    if (res->body) {
+        if (res->req->resp_limit) {
+            apr_off_t body_len = 0;
+            apr_brigade_length(res->body, 0, &body_len);
+            if (body_len + (apr_off_t)len > res->req->resp_limit) {
+                return 0; /* signal curl failure */
+            }
+        }
+        rv = apr_brigade_write(res->body, NULL, NULL, (const char *)data, blen);
+        if (rv != APR_SUCCESS) {
+            /* returning anything != blen will make CURL fail this */
+            return 0;
+        }
+    }
+    return blen;
+}
+
+static size_t header_cb(void *buffer, size_t elen, size_t nmemb, void *baton)
+{
+    md_http_response_t *res = baton;
+    size_t len, clen = elen * nmemb;
+    const char *name = NULL, *value = "", *b = buffer;
+    apr_size_t i;
+    
+    len = (clen && b[clen-1] == '\n')? clen-1 : clen;
+    len = (len && b[len-1] == '\r')? len-1 : len;
+    for (i = 0; i < len; ++i) {
+        if (b[i] == ':') {
+            name = apr_pstrndup(res->req->pool, b, i);
+            ++i;
+            while (i < len && b[i] == ' ') {
+                ++i;
+            }
+            if (i < len) {
+                value = apr_pstrndup(res->req->pool, b+i, len - i);
+            }
+            break;
+        }
+    }
+    
+    if (name != NULL) {
+        apr_table_add(res->headers, name, value);
+    }
+    return clen;
+}
+
+static apr_status_t curl_init(md_http_request_t *req)
+{
+    CURL *curl = curl_easy_init();
+    if (!curl) {
+        return APR_EGENERAL;
+    }
+    
+    curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_cb);
+    curl_easy_setopt(curl, CURLOPT_HEADERDATA, NULL);
+    curl_easy_setopt(curl, CURLOPT_READFUNCTION, req_data_cb);
+    curl_easy_setopt(curl, CURLOPT_READDATA, NULL);
+    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, resp_data_cb);
+    curl_easy_setopt(curl, CURLOPT_WRITEDATA, NULL);
+    
+    req->internals = curl;
+    return APR_SUCCESS;
+}
+
+typedef struct {
+    md_http_request_t *req;
+    struct curl_slist *hdrs;
+    apr_status_t rv;
+} curlify_hdrs_ctx;
+
+static int curlify_headers(void *baton, const char *key, const char *value)
+{
+    curlify_hdrs_ctx *ctx = baton;
+    const char *s;
+    
+    if (strchr(key, '\r') || strchr(key, '\n')
+        || strchr(value, '\r') || strchr(value, '\n')) {
+        ctx->rv = APR_EINVAL;
+        return 0;
+    }
+    s = apr_psprintf(ctx->req->pool, "%s: %s", key, value);
+    ctx->hdrs = curl_slist_append(ctx->hdrs, s);
+    return 1;
+}
+
+static apr_status_t curl_perform(md_http_request_t *req)
+{
+    apr_status_t rv = APR_SUCCESS;
+    CURLcode curle;
+    md_http_response_t *res;
+    CURL *curl;
+    struct curl_slist *req_hdrs = NULL;
+
+    rv = curl_init(req);
+    curl = req->internals;
+    
+    res = apr_pcalloc(req->pool, sizeof(*res));
+    
+    res->req = req;
+    res->rv = APR_SUCCESS;
+    res->status = 400;
+    res->headers = apr_table_make(req->pool, 5);
+    res->body = apr_brigade_create(req->pool, req->bucket_alloc);
+    
+    curl_easy_setopt(curl, CURLOPT_URL, req->url);
+    if (!apr_strnatcasecmp("GET", req->method)) {
+        /* nop */
+    }
+    else if (!apr_strnatcasecmp("HEAD", req->method)) {
+        curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
+    }
+    else if (!apr_strnatcasecmp("POST", req->method)) {
+        curl_easy_setopt(curl, CURLOPT_POST, 1L);
+    }
+    else {
+        curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, req->method);
+    }
+    curl_easy_setopt(curl, CURLOPT_HEADERDATA, res);
+    curl_easy_setopt(curl, CURLOPT_READDATA, req->body);
+    curl_easy_setopt(curl, CURLOPT_WRITEDATA, res);
+    
+    if (req->user_agent) {
+        curl_easy_setopt(curl, CURLOPT_USERAGENT, req->user_agent);
+    }
+    if (req->proxy_url) {
+        curl_easy_setopt(curl, CURLOPT_PROXY, req->proxy_url);
+    }
+    if (!apr_is_empty_table(req->headers)) {
+        curlify_hdrs_ctx ctx;
+        
+        ctx.req = req;
+        ctx.hdrs = NULL;
+        ctx.rv = APR_SUCCESS;
+        apr_table_do(curlify_headers, &ctx, req->headers, NULL);
+        req_hdrs = ctx.hdrs;
+        if (ctx.rv == APR_SUCCESS) {
+            curl_easy_setopt(curl, CURLOPT_HTTPHEADER, req_hdrs);
+        }
+    }
+    
+    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, req->pool, 
+                  "request %ld --> %s %s", req->id, req->method, req->url);
+    
+    if (md_log_is_level(req->pool, MD_LOG_TRACE3)) {
+        curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
+    }
+    
+    curle = curl_easy_perform(curl);
+    res->rv = curl_status(curle);
+    
+    if (APR_SUCCESS == res->rv) {
+        long l;
+        res->rv = curl_status(curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &l));
+        if (APR_SUCCESS == res->rv) {
+            res->status = (int)l;
+        }
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, res->rv, req->pool, 
+                      "request %ld <-- %d", req->id, res->status);
+    }
+    else {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, res->rv, req->pool, 
+                      "request %ld failed(%d): %s", req->id, curle, 
+                      curl_easy_strerror(curle));
+    }
+    
+    if (req->cb) {
+        res->rv = req->cb(res);
+    }
+    
+    rv = res->rv;
+    md_http_req_destroy(req);
+    if (req_hdrs) {
+        curl_slist_free_all(req_hdrs);
+    }
+    
+    return rv;
+}
+
+static int initialized;
+
+static apr_status_t md_curl_init(void) {
+    if (!initialized) {
+        initialized = 1;
+        curl_global_init(CURL_GLOBAL_DEFAULT);
+    }
+    return APR_SUCCESS;
+}
+
+static void curl_req_cleanup(md_http_request_t *req) 
+{
+    if (req->internals) {
+        curl_easy_cleanup(req->internals);
+        req->internals = NULL;
+    }
+}
+
+static md_http_impl_t impl = {
+    md_curl_init,
+    curl_req_cleanup,
+    curl_perform
+};
+
+md_http_impl_t * md_curl_get_impl(apr_pool_t *p)
+{
+    /* trigger early global curl init, before we are down a rabbit hole */
+    (void)p;
+    md_curl_init();
+    return &impl;
+}
diff --git a/modules/md/md_curl.h b/modules/md/md_curl.h
new file mode 100644 (file)
index 0000000..5b358cb
--- /dev/null
@@ -0,0 +1,23 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef md_curl_h
+#define md_curl_h
+
+struct md_http_impl;
+
+struct md_http_impl_t * md_curl_get_impl(apr_pool_t *p);
+
+#endif /* md_curl_h */
diff --git a/modules/md/md_http.c b/modules/md/md_http.c
new file mode 100644 (file)
index 0000000..6c784fc
--- /dev/null
@@ -0,0 +1,243 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_buckets.h>
+
+#include "md_http.h"
+#include "md_log.h"
+
+struct md_http_t {
+    apr_pool_t *pool;
+    apr_bucket_alloc_t *bucket_alloc;
+    apr_off_t resp_limit;
+    md_http_impl_t *impl;
+    const char *user_agent;
+    const char *proxy_url;
+};
+
+static md_http_impl_t *cur_impl;
+static int cur_init_done;
+
+void md_http_use_implementation(md_http_impl_t *impl)
+{
+    if (cur_impl != impl) {
+        cur_impl = impl;
+        cur_init_done = 0;
+    }
+}
+
+static long next_req_id;
+
+apr_status_t md_http_create(md_http_t **phttp, apr_pool_t *p, const char *user_agent,
+                            const char *proxy_url)
+{
+    md_http_t *http;
+    apr_status_t rv = APR_SUCCESS;
+
+    if (!cur_impl) {
+        *phttp = NULL;
+        return APR_ENOTIMPL;
+    }
+    
+    if (!cur_init_done) {
+        if (APR_SUCCESS == (rv = cur_impl->init())) {
+            cur_init_done = 1;
+        }
+        else {
+            return rv;
+        }
+    }
+    
+    http = apr_pcalloc(p, sizeof(*http));
+    http->pool = p;
+    http->impl = cur_impl;
+    http->user_agent = apr_pstrdup(p, user_agent);
+    http->proxy_url = proxy_url? apr_pstrdup(p, proxy_url) : NULL;
+    http->bucket_alloc = apr_bucket_alloc_create(p);
+    if (!http->bucket_alloc) {
+        return APR_EGENERAL;
+    }
+    *phttp = http;
+    return APR_SUCCESS;
+}
+
+void md_http_set_response_limit(md_http_t *http, apr_off_t resp_limit)
+{
+    http->resp_limit = resp_limit;
+}
+
+static apr_status_t req_create(md_http_request_t **preq, md_http_t *http, 
+                               const char *method, const char *url, struct apr_table_t *headers,
+                               md_http_cb *cb, void *baton)
+{
+    md_http_request_t *req;
+    apr_pool_t *pool;
+    apr_status_t rv;
+    
+    rv = apr_pool_create(&pool, http->pool);
+    if (rv != APR_SUCCESS) {
+        return rv;
+    }
+    
+    req = apr_pcalloc(pool, sizeof(*req));
+    req->id = next_req_id++;
+    req->pool = pool;
+    req->bucket_alloc = http->bucket_alloc;
+    req->http = http;
+    req->method = method;
+    req->url = url;
+    req->headers = headers? apr_table_copy(req->pool, headers) : apr_table_make(req->pool, 5);
+    req->resp_limit = http->resp_limit;
+    req->cb = cb;
+    req->baton = baton;
+    req->user_agent = http->user_agent;
+    req->proxy_url = http->proxy_url;
+
+    *preq = req;
+    return rv;
+}
+
+void md_http_req_destroy(md_http_request_t *req) 
+{
+    if (req->internals) {
+        req->http->impl->req_cleanup(req);
+        req->internals = NULL;
+    }
+    apr_pool_destroy(req->pool);
+}
+
+static apr_status_t schedule(md_http_request_t *req, 
+                             apr_bucket_brigade *body, int detect_clen,
+                             long *preq_id) 
+{
+    apr_status_t rv;
+    
+    req->body = body;
+    req->body_len = body? -1 : 0;
+
+    if (req->body && detect_clen) {
+        rv = apr_brigade_length(req->body, 1, &req->body_len);
+        if (rv != APR_SUCCESS) {
+            md_http_req_destroy(req);
+            return rv;
+        }
+    }
+    
+    if (req->body_len == 0 && apr_strnatcasecmp("GET", req->method)) {
+        apr_table_setn(req->headers, "Content-Length", "0");
+    }
+    else if (req->body_len > 0) {
+        apr_table_setn(req->headers, "Content-Length", apr_off_t_toa(req->pool, req->body_len));
+    }
+    
+    if (preq_id) {
+        *preq_id = req->id;
+    }
+    
+    /* we send right away */
+    rv = req->http->impl->perform(req);
+    
+    return rv;
+}
+
+apr_status_t md_http_GET(struct md_http_t *http, 
+                         const char *url, struct apr_table_t *headers,
+                         md_http_cb *cb, void *baton, long *preq_id)
+{
+    md_http_request_t *req;
+    apr_status_t rv;
+    
+    rv = req_create(&req, http, "GET", url, headers, cb, baton);
+    if (rv != APR_SUCCESS) {
+        return rv;
+    }
+    
+    return schedule(req, NULL, 0, preq_id);
+}
+
+apr_status_t md_http_HEAD(struct md_http_t *http, 
+                          const char *url, struct apr_table_t *headers,
+                          md_http_cb *cb, void *baton, long *preq_id)
+{
+    md_http_request_t *req;
+    apr_status_t rv;
+    
+    rv = req_create(&req, http, "HEAD", url, headers, cb, baton);
+    if (rv != APR_SUCCESS) {
+        return rv;
+    }
+    
+    return schedule(req, NULL, 0, preq_id);
+}
+
+apr_status_t md_http_POST(struct md_http_t *http, const char *url, 
+                          struct apr_table_t *headers, const char *content_type, 
+                          apr_bucket_brigade *body,
+                          md_http_cb *cb, void *baton, long *preq_id)
+{
+    md_http_request_t *req;
+    apr_status_t rv;
+    
+    rv = req_create(&req, http, "POST", url, headers, cb, baton);
+    if (rv != APR_SUCCESS) {
+        return rv;
+    }
+    
+    if (content_type) {
+        apr_table_set(req->headers, "Content-Type", content_type); 
+    }
+    return schedule(req, body, 1, preq_id);
+}
+
+apr_status_t md_http_POSTd(md_http_t *http, const char *url, 
+                           struct apr_table_t *headers, const char *content_type, 
+                           const char *data, size_t data_len, 
+                           md_http_cb *cb, void *baton, long *preq_id)
+{
+    md_http_request_t *req;
+    apr_status_t rv;
+    apr_bucket_brigade *body = NULL;
+    
+    rv = req_create(&req, http, "POST", url, headers, cb, baton);
+    if (rv != APR_SUCCESS) {
+        return rv;
+    }
+
+    if (data && data_len > 0) {
+        body = apr_brigade_create(req->pool, req->http->bucket_alloc);
+        rv = apr_brigade_write(body, NULL, NULL, data, data_len);
+        if (rv != APR_SUCCESS) {
+            md_http_req_destroy(req);
+            return rv;
+        }
+    }
+    
+    if (content_type) {
+        apr_table_set(req->headers, "Content-Type", content_type); 
+    }
+     
+    return schedule(req, body, 1, preq_id);
+}
+
+apr_status_t md_http_await(md_http_t *http, long req_id)
+{
+    (void)http;
+    (void)req_id;
+    return APR_SUCCESS;
+}
+
diff --git a/modules/md/md_http.h b/modules/md/md_http.h
new file mode 100644 (file)
index 0000000..874af4a
--- /dev/null
@@ -0,0 +1,101 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef mod_md_md_http_h
+#define mod_md_md_http_h
+
+struct apr_table_t;
+struct apr_bucket_brigade;
+struct apr_bucket_alloc_t;
+
+typedef struct md_http_t md_http_t;
+
+typedef struct md_http_request_t md_http_request_t;
+typedef struct md_http_response_t md_http_response_t;
+
+typedef apr_status_t md_http_cb(const md_http_response_t *res);
+
+struct md_http_request_t {
+    long id;
+    md_http_t *http;
+    apr_pool_t *pool;
+    struct apr_bucket_alloc_t *bucket_alloc;
+    const char *method;
+    const char *url;
+    const char *user_agent;
+    const char *proxy_url;
+    apr_table_t *headers;
+    struct apr_bucket_brigade *body;
+    apr_off_t body_len;
+    apr_off_t resp_limit;
+    md_http_cb *cb;
+    void *baton;
+    void *internals;
+};
+
+struct md_http_response_t {
+    md_http_request_t *req;
+    apr_status_t rv;
+    int status;
+    apr_table_t *headers;
+    struct apr_bucket_brigade *body;
+};
+
+apr_status_t md_http_create(md_http_t **phttp, apr_pool_t *p, const char *user_agent,
+                            const char *proxy_url);
+
+void md_http_set_response_limit(md_http_t *http, apr_off_t resp_limit);
+
+apr_status_t md_http_GET(md_http_t *http, 
+                         const char *url, struct apr_table_t *headers,
+                         md_http_cb *cb, void *baton, long *preq_id);
+
+apr_status_t md_http_HEAD(md_http_t *http, 
+                          const char *url, struct apr_table_t *headers,
+                          md_http_cb *cb, void *baton, long *preq_id);
+
+apr_status_t md_http_POST(md_http_t *http, const char *url, 
+                          struct apr_table_t *headers, const char *content_type, 
+                          struct apr_bucket_brigade *body,
+                          md_http_cb *cb, void *baton, long *preq_id);
+
+apr_status_t md_http_POSTd(md_http_t *http, const char *url, 
+                           struct apr_table_t *headers, const char *content_type, 
+                           const char *data, size_t data_len, 
+                           md_http_cb *cb, void *baton, long *preq_id);
+
+apr_status_t md_http_await(md_http_t *http, long req_id);
+
+void md_http_req_destroy(md_http_request_t *req);
+
+/**************************************************************************************************/
+/* interface to implementation */
+
+typedef apr_status_t md_http_init_cb(void);
+typedef void md_http_req_cleanup_cb(md_http_request_t *req);
+typedef apr_status_t md_http_perform_cb(md_http_request_t *req);
+
+typedef struct md_http_impl_t md_http_impl_t;
+struct md_http_impl_t {
+    md_http_init_cb *init;
+    md_http_req_cleanup_cb *req_cleanup;
+    md_http_perform_cb *perform;
+};
+
+void md_http_use_implementation(md_http_impl_t *impl);
+
+
+
+#endif /* md_http_h */
diff --git a/modules/md/md_json.c b/modules/md/md_json.c
new file mode 100644 (file)
index 0000000..bbbefd9
--- /dev/null
@@ -0,0 +1,1031 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_buckets.h>
+
+#include "md_json.h"
+#include "md_log.h"
+#include "md_http.h"
+#include "md_util.h"
+
+/* jansson thinks everyone compiles with the platform's cc in its fullest capabilities
+ * when undefining their INLINEs, we get static, unused functions, arg 
+ */
+#if defined(__GNUC__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunreachable-code"
+#endif
+#if defined(__clang__)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunused-function"
+#endif
+
+#include <jansson_config.h>
+#undef  JSON_INLINE
+#define JSON_INLINE 
+#include <jansson.h>
+
+#if defined(__GNUC__)
+#pragma GCC diagnostic pop
+#endif
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#endif
+
+struct md_json_t {
+    apr_pool_t *p;
+    json_t *j;
+};
+
+/**************************************************************************************************/
+/* lifecycle */
+
+static apr_status_t json_pool_cleanup(void *data)
+{
+    md_json_t *json = data;
+    if (json) {
+        md_json_destroy(json);
+    }
+    return APR_SUCCESS;
+}
+
+static md_json_t *json_create(apr_pool_t *pool, json_t *j)
+{
+    md_json_t *json;
+    
+    if (!j) {
+        apr_abortfunc_t abfn = apr_pool_abort_get(pool);
+        if (abfn) {
+            abfn(APR_ENOMEM);
+        }
+        assert(j != NULL); /* failsafe in case abort is unset */
+    }
+    json = apr_pcalloc(pool, sizeof(*json));
+    json->p = pool;
+    json->j = j;
+    apr_pool_cleanup_register(pool, json, json_pool_cleanup, apr_pool_cleanup_null);
+        
+    return json;
+}
+
+md_json_t *md_json_create(apr_pool_t *pool)
+{
+    return json_create(pool, json_object());
+}
+
+md_json_t *md_json_create_s(apr_pool_t *pool, const char *s)
+{
+    return json_create(pool, json_string(s));
+}
+
+void md_json_destroy(md_json_t *json)
+{
+    if (json && json->j) {
+        json_decref(json->j);
+        json->j = NULL;
+    }
+}
+
+md_json_t *md_json_copy(apr_pool_t *pool, md_json_t *json)
+{
+    return json_create(pool, json_copy(json->j));
+}
+
+md_json_t *md_json_clone(apr_pool_t *pool, md_json_t *json)
+{
+    return json_create(pool, json_deep_copy(json->j));
+}
+
+/**************************************************************************************************/
+/* selectors */
+
+
+static json_t *jselect(md_json_t *json, va_list ap)
+{
+    json_t *j;
+    const char *key;
+    
+    j = json->j;
+    key = va_arg(ap, char *);
+    while (key && j) {
+        j = json_object_get(j, key);
+        key = va_arg(ap, char *);
+    }
+    return j;
+}
+
+static json_t *jselect_parent(const char **child_key, int create, md_json_t *json, va_list ap)
+{
+    const char *key, *next;
+    json_t *j, *jn;
+    
+    *child_key = NULL;
+    j = json->j;
+    key = va_arg(ap, char *);
+    while (key && j) {
+        next = va_arg(ap, char *);
+        if (next) {
+            jn = json_object_get(j, key);
+            if (!jn && create) {
+                jn = json_object();
+                json_object_set_new(j, key, jn);
+            }
+            j = jn;
+        }
+        else {
+            *child_key = key;
+        }
+        key = next;
+    }
+    return j;
+}
+
+static apr_status_t jselect_add(json_t *val, md_json_t *json, va_list ap)
+{
+    const char *key;
+    json_t *j, *aj;
+    
+    j = jselect_parent(&key, 1, json, ap);
+    
+    if (!j || !json_is_object(j)) {
+        json_decref(val);
+        return APR_EINVAL;
+    }
+    
+    aj = json_object_get(j, key);
+    if (!aj) {
+        aj = json_array();
+        json_object_set_new(j, key, aj);
+    }
+    
+    if (!json_is_array(aj)) {
+        json_decref(val);
+        return APR_EINVAL;
+    }
+
+    json_array_append(aj, val);
+    return APR_SUCCESS;
+}
+
+static apr_status_t jselect_set(json_t *val, md_json_t *json, va_list ap)
+{
+    const char *key;
+    json_t *j;
+    
+    j = jselect_parent(&key, 1, json, ap);
+    
+    if (!j) {
+        json_decref(val);
+        return APR_EINVAL;
+    }
+    
+    if (key) {
+        if (!json_is_object(j)) {
+            json_decref(val);
+            return APR_EINVAL;
+        }
+        json_object_set(j, key, val);
+    }
+    else {
+        /* replace */
+        if (json->j) {
+            json_decref(json->j);
+        }
+        json_incref(val);
+        json->j = val;
+    }
+    return APR_SUCCESS;
+}
+
+static apr_status_t jselect_set_new(json_t *val, md_json_t *json, va_list ap)
+{
+    const char *key;
+    json_t *j;
+    
+    j = jselect_parent(&key, 1, json, ap);
+    
+    if (!j) {
+        json_decref(val);
+        return APR_EINVAL;
+    }
+    
+    if (key) {
+        if (!json_is_object(j)) {
+            json_decref(val);
+            return APR_EINVAL;
+        }
+        json_object_set_new(j, key, val);
+    }
+    else {
+        /* replace */
+        if (json->j) {
+            json_decref(json->j);
+        }
+        json->j = val;
+    }
+    return APR_SUCCESS;
+}
+
+int md_json_has_key(md_json_t *json, ...)
+{
+    json_t *j;
+    va_list ap;
+    
+    va_start(ap, json);
+    j = jselect(json, ap);
+    va_end(ap);
+
+    return j != NULL;
+}
+
+/**************************************************************************************************/
+/* booleans */
+
+int md_json_getb(md_json_t *json, ...)
+{
+    json_t *j;
+    va_list ap;
+    
+    va_start(ap, json);
+    j = jselect(json, ap);
+    va_end(ap);
+
+    return j? json_is_true(j) : 0;
+}
+
+apr_status_t md_json_setb(int value, md_json_t *json, ...)
+{
+    va_list ap;
+    apr_status_t rv;
+    
+    va_start(ap, json);
+    rv = jselect_set_new(json_boolean(value), json, ap);
+    va_end(ap);
+    return rv;
+}
+
+/**************************************************************************************************/
+/* numbers */
+
+double md_json_getn(md_json_t *json, ...)
+{
+    json_t *j;
+    va_list ap;
+    
+    va_start(ap, json);
+    j = jselect(json, ap);
+    va_end(ap);
+    return (j && json_is_number(j))? json_number_value(j) : 0.0;
+}
+
+apr_status_t md_json_setn(double value, md_json_t *json, ...)
+{
+    va_list ap;
+    apr_status_t rv;
+    
+    va_start(ap, json);
+    rv = jselect_set_new(json_real(value), json, ap);
+    va_end(ap);
+    return rv;
+}
+
+/**************************************************************************************************/
+/* longs */
+
+long md_json_getl(md_json_t *json, ...)
+{
+    json_t *j;
+    va_list ap;
+    
+    va_start(ap, json);
+    j = jselect(json, ap);
+    va_end(ap);
+    return (long)((j && json_is_number(j))? json_integer_value(j) : 0L);
+}
+
+apr_status_t md_json_setl(long value, md_json_t *json, ...)
+{
+    va_list ap;
+    apr_status_t rv;
+    
+    va_start(ap, json);
+    rv = jselect_set_new(json_integer(value), json, ap);
+    va_end(ap);
+    return rv;
+}
+
+/**************************************************************************************************/
+/* strings */
+
+const char *md_json_gets(md_json_t *json, ...)
+{
+    json_t *j;
+    va_list ap;
+    
+    va_start(ap, json);
+    j = jselect(json, ap);
+    va_end(ap);
+
+    return (j && json_is_string(j))? json_string_value(j) : NULL;
+}
+
+const char *md_json_dups(apr_pool_t *p, md_json_t *json, ...)
+{
+    json_t *j;
+    va_list ap;
+    
+    va_start(ap, json);
+    j = jselect(json, ap);
+    va_end(ap);
+
+    return (j && json_is_string(j))? apr_pstrdup(p, json_string_value(j)) : NULL;
+}
+
+apr_status_t md_json_sets(const char *value, md_json_t *json, ...)
+{
+    va_list ap;
+    apr_status_t rv;
+    
+    va_start(ap, json);
+    rv = jselect_set_new(json_string(value), json, ap);
+    va_end(ap);
+    return rv;
+}
+
+/**************************************************************************************************/
+/* json itself */
+
+md_json_t *md_json_getj(md_json_t *json, ...)
+{
+    json_t *j;
+    va_list ap;
+    
+    va_start(ap, json);
+    j = jselect(json, ap);
+    va_end(ap);
+    
+    if (j) {
+        if (j == json->j) {
+            return json;
+        }
+        json_incref(j);
+        return json_create(json->p, j);
+    }
+    return NULL;
+}
+
+apr_status_t md_json_setj(md_json_t *value, md_json_t *json, ...)
+{
+    va_list ap;
+    apr_status_t rv;
+    const char *key;
+    json_t *j;
+    
+    if (value) {
+        va_start(ap, json);
+        rv = jselect_set(value->j, json, ap);
+        va_end(ap);
+    }
+    else {
+        va_start(ap, json);
+        j = jselect_parent(&key, 1, json, ap);
+        va_end(ap);
+        
+        if (key && j && !json_is_object(j)) {
+            json_object_del(j, key);
+            rv = APR_SUCCESS;
+        }
+        else {
+            rv = APR_EINVAL;
+        }
+    }
+    return rv;
+}
+
+apr_status_t md_json_addj(md_json_t *value, md_json_t *json, ...)
+{
+    va_list ap;
+    apr_status_t rv;
+    
+    va_start(ap, json);
+    rv = jselect_add(value->j, json, ap);
+    va_end(ap);
+    return rv;
+}
+
+
+/**************************************************************************************************/
+/* arrays / objects */
+
+apr_status_t md_json_clr(md_json_t *json, ...)
+{
+    json_t *j;
+    va_list ap;
+    
+    va_start(ap, json);
+    j = jselect(json, ap);
+    va_end(ap);
+
+    if (j && json_is_object(j)) {
+        json_object_clear(j);
+    }
+    else if (j && json_is_array(j)) {
+        json_array_clear(j);
+    }
+    return APR_SUCCESS;
+}
+
+apr_status_t md_json_del(md_json_t *json, ...)
+{
+    const char *key;
+    json_t *j;
+    va_list ap;
+    
+    va_start(ap, json);
+    j = jselect_parent(&key, 0, json, ap);
+    va_end(ap);
+    
+    if (key && j && json_is_object(j)) {
+        json_object_del(j, key);
+    }
+    return APR_SUCCESS;
+}
+
+/**************************************************************************************************/
+/* object strings */
+
+apr_status_t md_json_gets_dict(apr_table_t *dict, md_json_t *json, ...)
+{
+    json_t *j;
+    va_list ap;
+    
+    va_start(ap, json);
+    j = jselect(json, ap);
+    va_end(ap);
+
+    if (j && json_is_object(j)) {
+        const char *key;
+        json_t *val;
+        
+        json_object_foreach(j, key, val) {
+            if (json_is_string(val)) {
+                apr_table_set(dict, key, json_string_value(val));
+            }
+        }
+        return APR_SUCCESS;
+    }
+    return APR_ENOENT;
+}
+
+static int object_set(void *data, const char *key, const char *val)
+{
+    json_t *j = data, *nj = json_string(val);
+    json_object_set(j, key, nj);
+    json_decref(nj);
+    return 1;
+}
+apr_status_t md_json_sets_dict(apr_table_t *dict, md_json_t *json, ...)
+{
+    json_t *nj, *j;
+    va_list ap;
+    
+    va_start(ap, json);
+    j = jselect(json, ap);
+    va_end(ap);
+    
+    if (!j || !json_is_object(j)) {
+        const char *key;
+        
+        va_start(ap, json);
+        j = jselect_parent(&key, 1, json, ap);
+        va_end(ap);
+        
+        if (!key || !j || !json_is_object(j)) {
+            return APR_EINVAL;
+        }
+        nj = json_object();
+        json_object_set_new(j, key, nj);
+        j = nj; 
+    }
+    
+    apr_table_do(object_set, j, dict, NULL);
+    return APR_SUCCESS;
+}
+
+/**************************************************************************************************/
+/* conversions */
+
+apr_status_t md_json_pass_to(void *value, md_json_t *json, apr_pool_t *p, void *baton)
+{
+    (void)p;
+    (void)baton;
+    return md_json_setj(value, json, NULL);
+}
+
+apr_status_t md_json_pass_from(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton)
+{
+    (void)p;
+    (void)baton;
+    *pvalue = json;
+    return APR_SUCCESS;
+}
+
+apr_status_t md_json_clone_to(void *value, md_json_t *json, apr_pool_t *p, void *baton)
+{
+    (void)baton;
+    return md_json_setj(md_json_clone(p, value), json, NULL);
+}
+
+apr_status_t md_json_clone_from(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton)
+{
+    (void)baton;
+    *pvalue = md_json_clone(p, json);
+    return APR_SUCCESS;
+}
+
+/**************************************************************************************************/
+/* array generic */
+
+apr_status_t md_json_geta(apr_array_header_t *a, md_json_from_cb *cb, void *baton,
+                          md_json_t *json, ...)
+{
+    json_t *j;
+    va_list ap;
+    apr_status_t rv = APR_SUCCESS;
+    size_t index;
+    json_t *val;
+    md_json_t wrap;
+    void *element;
+    
+    va_start(ap, json);
+    j = jselect(json, ap);
+    va_end(ap);
+    
+    if (!j || !json_is_array(j)) {
+        return APR_ENOENT;
+    }
+        
+    wrap.p = a->pool;
+    json_array_foreach(j, index, val) {
+        wrap.j = val;
+        if (APR_SUCCESS == (rv = cb(&element, &wrap, wrap.p, baton))) {
+            if (element) {
+                APR_ARRAY_PUSH(a, void*) = element;
+            }
+        }
+        else if (APR_ENOENT == rv) {
+            rv = APR_SUCCESS;
+        }
+        else {
+            break;
+        }
+    }
+    return rv;
+}
+
+apr_status_t md_json_seta(apr_array_header_t *a, md_json_to_cb *cb, void *baton, 
+                          md_json_t *json, ...)
+{
+    json_t *j, *nj;
+    md_json_t wrap;
+    apr_status_t rv = APR_SUCCESS;
+    va_list ap;
+    int i;
+    
+    va_start(ap, json);
+    j = jselect(json, ap);
+    va_end(ap);
+    
+    if (!j || !json_is_array(j)) {
+        const char *key;
+        
+        va_start(ap, json);
+        j = jselect_parent(&key, 1, json, ap);
+        va_end(ap);
+        
+        if (!key || !j || !json_is_object(j)) {
+            return APR_EINVAL;
+        }
+        nj = json_array();
+        json_object_set_new(j, key, nj);
+        j = nj; 
+    }
+    
+    json_array_clear(j);
+    wrap.p = json->p;
+    for (i = 0; i < a->nelts; ++i) {
+        if (!cb) {
+            return APR_EINVAL;
+        }    
+        wrap.j = json_string("");
+        if (APR_SUCCESS == (rv = cb(APR_ARRAY_IDX(a, i, void*), &wrap, json->p, baton))) {
+            json_array_append_new(j, wrap.j);
+        }
+    }
+    return rv;
+}
+
+int md_json_itera(md_json_itera_cb *cb, void *baton, md_json_t *json, ...)
+{
+    json_t *j;
+    va_list ap;
+    size_t index;
+    json_t *val;
+    md_json_t wrap;
+    
+    va_start(ap, json);
+    j = jselect(json, ap);
+    va_end(ap);
+    
+    if (!j || !json_is_array(j)) {
+        return 0;
+    }
+        
+    wrap.p = json->p;
+    json_array_foreach(j, index, val) {
+        wrap.j = val;
+        if (!cb(baton, index, &wrap)) {
+            return 0;
+        }
+    }
+    return 1;
+}
+
+/**************************************************************************************************/
+/* array strings */
+
+apr_status_t md_json_getsa(apr_array_header_t *a, md_json_t *json, ...)
+{
+    json_t *j;
+    va_list ap;
+    
+    va_start(ap, json);
+    j = jselect(json, ap);
+    va_end(ap);
+
+    if (j && json_is_array(j)) {
+        size_t index;
+        json_t *val;
+        
+        json_array_foreach(j, index, val) {
+            if (json_is_string(val)) {
+                APR_ARRAY_PUSH(a, const char *) = json_string_value(val);
+            }
+        }
+        return APR_SUCCESS;
+    }
+    return APR_ENOENT;
+}
+
+apr_status_t md_json_dupsa(apr_array_header_t *a, apr_pool_t *p, md_json_t *json, ...)
+{
+    json_t *j;
+    va_list ap;
+    
+    va_start(ap, json);
+    j = jselect(json, ap);
+    va_end(ap);
+
+    if (j && json_is_array(j)) {
+        size_t index;
+        json_t *val;
+        
+        json_array_foreach(j, index, val) {
+            if (json_is_string(val)) {
+                APR_ARRAY_PUSH(a, const char *) = apr_pstrdup(p, json_string_value(val));
+            }
+        }
+        return APR_SUCCESS;
+    }
+    return APR_ENOENT;
+}
+
+apr_status_t md_json_setsa(apr_array_header_t *a, md_json_t *json, ...)
+{
+    json_t *nj, *j;
+    va_list ap;
+    int i;
+    
+    va_start(ap, json);
+    j = jselect(json, ap);
+    va_end(ap);
+    
+    if (!j || !json_is_array(j)) {
+        const char *key;
+        
+        va_start(ap, json);
+        j = jselect_parent(&key, 1, json, ap);
+        va_end(ap);
+        
+        if (!key || !j || !json_is_object(j)) {
+            return APR_EINVAL;
+        }
+        nj = json_array();
+        json_object_set_new(j, key, nj);
+        j = nj; 
+    }
+    
+    json_array_clear(j);
+    for (i = 0; i < a->nelts; ++i) {
+        json_array_append_new(j, json_string(APR_ARRAY_IDX(a, i, const char*)));
+    }
+    return APR_SUCCESS;
+}
+
+/**************************************************************************************************/
+/* formatting, parsing */
+
+typedef struct {
+    md_json_t *json;
+    md_json_fmt_t fmt;
+    const char *fname;
+    apr_file_t *f;
+} j_write_ctx;
+
+/* Convert from md_json_fmt_t to the Jansson json_dumpX flags. */
+static size_t fmt_to_flags(md_json_fmt_t fmt)
+{
+    /* NOTE: JSON_PRESERVE_ORDER is off by default before Jansson 2.8. It
+     * doesn't have any semantic effect on the protocol, but it does let the
+     * md_json_writeX unit tests run deterministically. */
+    return JSON_PRESERVE_ORDER |
+           ((fmt == MD_JSON_FMT_COMPACT) ? JSON_COMPACT : JSON_INDENT(2)); 
+}
+
+static int dump_cb(const char *buffer, size_t len, void *baton)
+{
+    apr_bucket_brigade *bb = baton;
+    apr_status_t rv;
+    
+    rv = apr_brigade_write(bb, NULL, NULL, buffer, len);
+    return (rv == APR_SUCCESS)? 0 : -1;
+}
+
+apr_status_t md_json_writeb(md_json_t *json, md_json_fmt_t fmt, apr_bucket_brigade *bb)
+{
+    int rv = json_dump_callback(json->j, dump_cb, bb, fmt_to_flags(fmt));
+    return rv? APR_EGENERAL : APR_SUCCESS;
+}
+
+static int chunk_cb(const char *buffer, size_t len, void *baton)
+{
+    apr_array_header_t *chunks = baton;
+    char *chunk = apr_pcalloc(chunks->pool, len+1);
+    
+    memcpy(chunk, buffer, len);
+    APR_ARRAY_PUSH(chunks, const char *) = chunk;
+    return 0;
+}
+
+const char *md_json_writep(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt)
+{
+    apr_array_header_t *chunks;
+    int rv;
+
+    chunks = apr_array_make(p, 10, sizeof(char *));
+    rv = json_dump_callback(json->j, chunk_cb, chunks, fmt_to_flags(fmt));
+
+    if (rv) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p,
+                      "md_json_writep failed to dump JSON");
+        return NULL;
+    }
+
+    switch (chunks->nelts) {
+        case 0:
+            return "";
+        case 1:
+            return APR_ARRAY_IDX(chunks, 0, const char *);
+        default:
+            return apr_array_pstrcat(p, chunks, 0);
+    }
+}
+
+apr_status_t md_json_writef(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, apr_file_t *f)
+{
+    apr_status_t rv;
+    const char *s;
+    
+    s = md_json_writep(json, p, fmt);
+
+    if (s) {
+        rv = apr_file_write_full(f, s, strlen(s), NULL);
+    }
+    else {
+        rv = APR_EINVAL;
+    }
+
+    if (APR_SUCCESS != rv) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, json->p, "md_json_writef");
+    }
+    return rv;
+}
+
+apr_status_t md_json_fcreatex(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, 
+                              const char *fpath, apr_fileperms_t perms)
+{
+    apr_status_t rv;
+    apr_file_t *f;
+    
+    rv = md_util_fcreatex(&f, fpath, perms, p);
+    if (APR_SUCCESS == rv) {
+        rv = md_json_writef(json, p, fmt, f);
+        apr_file_close(f);
+    }
+    return rv;
+}
+
+static apr_status_t write_json(void *baton, apr_file_t *f, apr_pool_t *p)
+{
+    j_write_ctx *ctx = baton;
+    apr_status_t rv = md_json_writef(ctx->json, p, ctx->fmt, f);
+    if (APR_SUCCESS != rv) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "freplace json in %s", ctx->fname);
+    }
+    return rv;
+}
+
+apr_status_t md_json_freplace(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, 
+                              const char *fpath, apr_fileperms_t perms)
+{
+    j_write_ctx ctx;
+    ctx.json = json;
+    ctx.fmt = fmt;
+    ctx.fname = fpath;
+    return md_util_freplace(fpath, perms, p, write_json, &ctx);
+}
+
+apr_status_t md_json_readd(md_json_t **pjson, apr_pool_t *pool, const char *data, size_t data_len)
+{
+    json_error_t error;
+    json_t *j;
+    
+    j = json_loadb(data, data_len, 0, &error);
+    if (!j) {
+        return APR_EINVAL;
+    }
+    *pjson = json_create(pool, j);
+    return APR_SUCCESS;
+}
+
+static size_t load_cb(void *data, size_t max_len, void *baton)
+{
+    apr_bucket_brigade *body = baton;
+    size_t blen, read_len = 0;
+    const char *bdata;
+    char *dest = data;
+    apr_bucket *b;
+    apr_status_t rv;
+    
+    while (body && !APR_BRIGADE_EMPTY(body) && max_len > 0) {
+        b = APR_BRIGADE_FIRST(body);
+        if (APR_BUCKET_IS_METADATA(b)) {
+            if (APR_BUCKET_IS_EOS(b)) {
+                body = NULL;
+            }
+        }
+        else {
+            rv = apr_bucket_read(b, &bdata, &blen, APR_BLOCK_READ);
+            if (rv == APR_SUCCESS) {
+                if (blen > max_len) {
+                    apr_bucket_split(b, max_len);
+                    blen = max_len;
+                }
+                memcpy(dest, bdata, blen);
+                read_len += blen;
+                max_len -= blen;
+                dest += blen;
+            }
+            else {
+                body = NULL;
+                if (!APR_STATUS_IS_EOF(rv)) {
+                    /* everything beside EOF is an error */
+                    read_len = (size_t)-1;
+                }
+            }
+        }
+        APR_BUCKET_REMOVE(b);
+        apr_bucket_delete(b);
+    }
+    
+    return read_len;
+}
+
+apr_status_t md_json_readb(md_json_t **pjson, apr_pool_t *pool, apr_bucket_brigade *bb)
+{
+    json_error_t error;
+    json_t *j;
+    
+    j = json_load_callback(load_cb, bb, 0, &error);
+    if (!j) {
+        return APR_EINVAL;
+    }
+    *pjson = json_create(pool, j);
+    return APR_SUCCESS;
+}
+
+static size_t load_file_cb(void *data, size_t max_len, void *baton)
+{
+    apr_file_t *f = baton;
+    apr_size_t len = max_len;
+    apr_status_t rv;
+    
+    rv = apr_file_read(f, data, &len);
+    if (APR_SUCCESS == rv) {
+        return len;
+    }
+    else if (APR_EOF == rv) {
+        return 0;
+    }
+    return (size_t)-1;
+}
+
+apr_status_t md_json_readf(md_json_t **pjson, apr_pool_t *p, const char *fpath)
+{
+    apr_file_t *f;
+    json_t *j;
+    apr_status_t rv;
+    json_error_t error;
+    
+    rv = apr_file_open(&f, fpath, APR_FOPEN_READ, 0, p);
+    if (rv != APR_SUCCESS) {
+        return rv;
+    }
+
+    j = json_load_callback(load_file_cb, f, 0, &error);
+    if (j) {
+        *pjson = json_create(p, j);
+    }
+    else {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p,
+                      "failed to load JSON file %s: %s (line %d:%d)",
+                      fpath, error.text, error.line, error.column);
+    }
+
+    apr_file_close(f);
+    return (j && *pjson) ? APR_SUCCESS : APR_EINVAL;
+}
+
+/**************************************************************************************************/
+/* http get */
+
+apr_status_t md_json_read_http(md_json_t **pjson, apr_pool_t *pool, const md_http_response_t *res)
+{
+    apr_status_t rv = APR_ENOENT;
+    if (res->rv == APR_SUCCESS) {
+        const char *ctype = apr_table_get(res->headers, "content-type");
+        if (ctype && res->body && (strstr(ctype, "/json") || strstr(ctype, "+json"))) {
+            rv = md_json_readb(pjson, pool, res->body);
+        }
+    }
+    return rv;
+}
+
+typedef struct {
+    apr_status_t rv;
+    apr_pool_t *pool;
+    md_json_t *json;
+} resp_data;
+
+static apr_status_t json_resp_cb(const md_http_response_t *res)
+{
+    resp_data *resp = res->req->baton;
+    return md_json_read_http(&resp->json, resp->pool, res);
+}
+
+apr_status_t md_json_http_get(md_json_t **pjson, apr_pool_t *pool,
+                              struct md_http_t *http, const char *url)
+{
+    long req_id;
+    apr_status_t rv;
+    resp_data resp;
+    
+    memset(&resp, 0, sizeof(resp));
+    resp.pool = pool;
+    
+    rv = md_http_GET(http, url, NULL, json_resp_cb, &resp, &req_id);
+    
+    if (rv == APR_SUCCESS) {
+        md_http_await(http, req_id);
+        *pjson = resp.json;
+        return resp.rv;
+    }
+    *pjson = NULL;
+    return rv;
+}
+
diff --git a/modules/md/md_json.h b/modules/md/md_json.h
new file mode 100644 (file)
index 0000000..dde3d24
--- /dev/null
@@ -0,0 +1,121 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef mod_md_md_json_h
+#define mod_md_md_json_h
+
+#include <apr_file_io.h>
+
+struct apr_bucket_brigade;
+struct apr_file_t;
+
+struct md_http_t;
+struct md_http_response_t;
+
+
+typedef struct md_json_t md_json_t;
+
+typedef enum {
+    MD_JSON_FMT_COMPACT,
+    MD_JSON_FMT_INDENT,
+} md_json_fmt_t;
+
+md_json_t *md_json_create(apr_pool_t *pool);
+void md_json_destroy(md_json_t *json);
+
+md_json_t *md_json_copy(apr_pool_t *pool, md_json_t *json);
+md_json_t *md_json_clone(apr_pool_t *pool, md_json_t *json);
+
+int md_json_has_key(md_json_t *json, ...);
+
+/* boolean manipulation */
+int md_json_getb(md_json_t *json, ...);
+apr_status_t md_json_setb(int value, md_json_t *json, ...);
+
+/* number manipulation */
+double md_json_getn(md_json_t *json, ...);
+apr_status_t md_json_setn(double value, md_json_t *json, ...);
+
+/* long manipulation */
+long md_json_getl(md_json_t *json, ...);
+apr_status_t md_json_setl(long value, md_json_t *json, ...);
+
+/* string manipulation */
+md_json_t *md_json_create_s(apr_pool_t *pool, const char *s);
+const char *md_json_gets(md_json_t *json, ...);
+const char *md_json_dups(apr_pool_t *p, md_json_t *json, ...);
+apr_status_t md_json_sets(const char *s, md_json_t *json, ...);
+
+/* json manipulation */
+md_json_t *md_json_getj(md_json_t *json, ...);
+apr_status_t md_json_setj(md_json_t *value, md_json_t *json, ...);
+apr_status_t md_json_addj(md_json_t *value, md_json_t *json, ...);
+
+/* Array/Object manipulation */
+apr_status_t md_json_clr(md_json_t *json, ...);
+apr_status_t md_json_del(md_json_t *json, ...);
+
+/* conversion function from and to json */
+typedef apr_status_t md_json_to_cb(void *value, md_json_t *json, apr_pool_t *p, void *baton);
+typedef apr_status_t md_json_from_cb(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton);
+
+/* identity pass through from json to json */
+apr_status_t md_json_pass_to(void *value, md_json_t *json, apr_pool_t *p, void *baton);
+apr_status_t md_json_pass_from(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton);
+
+/* conversions from json to json in specified pool */
+apr_status_t md_json_clone_to(void *value, md_json_t *json, apr_pool_t *p, void *baton);
+apr_status_t md_json_clone_from(void **pvalue, md_json_t *json, apr_pool_t *p, void *baton);
+
+/* Manipulating/Iteration on generic Arrays */
+apr_status_t md_json_geta(apr_array_header_t *a, md_json_from_cb *cb, 
+                          void *baton, md_json_t *json, ...);
+apr_status_t md_json_seta(apr_array_header_t *a, md_json_to_cb *cb, 
+                          void *baton, md_json_t *json, ...);
+
+typedef int md_json_itera_cb(void *baton, size_t index, md_json_t *json);
+int md_json_itera(md_json_itera_cb *cb, void *baton, md_json_t *json, ...);
+
+/* Manipulating Object String values */
+apr_status_t md_json_gets_dict(apr_table_t *dict, md_json_t *json, ...);
+apr_status_t md_json_sets_dict(apr_table_t *dict, md_json_t *json, ...);
+
+/* Manipulating String Arrays */
+apr_status_t md_json_getsa(apr_array_header_t *a, md_json_t *json, ...);
+apr_status_t md_json_dupsa(apr_array_header_t *a, apr_pool_t *p, md_json_t *json, ...);
+apr_status_t md_json_setsa(apr_array_header_t *a, md_json_t *json, ...);
+
+/* serialization & parsing */
+apr_status_t md_json_writeb(md_json_t *json, md_json_fmt_t fmt, struct apr_bucket_brigade *bb);
+const char *md_json_writep(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt);
+apr_status_t md_json_writef(md_json_t *json, apr_pool_t *p, 
+                            md_json_fmt_t fmt, struct apr_file_t *f);
+apr_status_t md_json_fcreatex(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, 
+                              const char *fpath, apr_fileperms_t perms);
+apr_status_t md_json_freplace(md_json_t *json, apr_pool_t *p, md_json_fmt_t fmt, 
+                              const char *fpath, apr_fileperms_t perms);
+
+apr_status_t md_json_readb(md_json_t **pjson, apr_pool_t *pool, struct apr_bucket_brigade *bb);
+apr_status_t md_json_readd(md_json_t **pjson, apr_pool_t *pool, const char *data, size_t data_len);
+apr_status_t md_json_readf(md_json_t **pjson, apr_pool_t *pool, const char *fpath);
+
+
+/* http retrieval */
+apr_status_t md_json_http_get(md_json_t **pjson, apr_pool_t *pool,
+                              struct md_http_t *http, const char *url);
+apr_status_t md_json_read_http(md_json_t **pjson, apr_pool_t *pool, 
+                               const struct md_http_response_t *res);
+
+#endif /* md_json_h */
diff --git a/modules/md/md_jws.c b/modules/md/md_jws.c
new file mode 100644 (file)
index 0000000..f3788b3
--- /dev/null
@@ -0,0 +1,105 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_tables.h>
+#include <apr_buckets.h>
+
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_jws.h"
+#include "md_log.h"
+#include "md_util.h"
+
+static int header_set(void *data, const char *key, const char *val)
+{
+    md_json_sets(val, (md_json_t *)data, key, NULL);
+    return 1;
+}
+
+apr_status_t md_jws_sign(md_json_t **pmsg, apr_pool_t *p,
+                         const char *payload, size_t len, 
+                         struct apr_table_t *protected, 
+                         struct md_pkey_t *pkey, const char *key_id)
+{
+    md_json_t *msg, *jprotected;
+    const char *prot64, *pay64, *sign64, *sign, *prot;
+    apr_status_t rv = APR_SUCCESS;
+
+    *pmsg = NULL;
+    
+    msg = md_json_create(p);
+
+    jprotected = md_json_create(p);
+    md_json_sets("RS256", jprotected, "alg", NULL);
+    if (key_id) {
+        md_json_sets(key_id, jprotected, "kid", NULL);
+    }
+    else {
+        md_json_sets(md_pkey_get_rsa_e64(pkey, p), jprotected, "jwk", "e", NULL);
+        md_json_sets("RSA", jprotected, "jwk", "kty", NULL);
+        md_json_sets(md_pkey_get_rsa_n64(pkey, p), jprotected, "jwk", "n", NULL);
+    }
+    apr_table_do(header_set, jprotected, protected, NULL);
+    prot = md_json_writep(jprotected, p, MD_JSON_FMT_COMPACT);
+    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE4, 0, p, "protected: %s",
+                  prot ? prot : "<failed to serialize!>");
+
+    if (!prot) {
+        rv = APR_EINVAL;
+    }
+    
+    if (rv == APR_SUCCESS) {
+        prot64 = md_util_base64url_encode(prot, strlen(prot), p);
+        md_json_sets(prot64, msg, "protected", NULL);
+        pay64 = md_util_base64url_encode(payload, len, p);
+
+        md_json_sets(pay64, msg, "payload", NULL);
+        sign = apr_psprintf(p, "%s.%s", prot64, pay64);
+
+        rv = md_crypt_sign64(&sign64, pkey, p, sign, strlen(sign));
+    }
+
+    if (rv == APR_SUCCESS) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, 
+                      "jws pay64=%s\nprot64=%s\nsign64=%s", pay64, prot64, sign64);
+        
+        md_json_sets(sign64, msg, "signature", NULL);
+    }
+    else {
+        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "jwk signed message");
+    } 
+    
+    *pmsg = (APR_SUCCESS == rv)? msg : NULL;
+    return rv;
+}
+
+apr_status_t md_jws_pkey_thumb(const char **pthumb, apr_pool_t *p, struct md_pkey_t *pkey)
+{
+    const char *e64, *n64, *s;
+    apr_status_t rv;
+    
+    e64 = md_pkey_get_rsa_e64(pkey, p);
+    n64 = md_pkey_get_rsa_n64(pkey, p);
+    if (!e64 || !n64) {
+        return APR_EINVAL;
+    }
+
+    /* whitespace and order is relevant, since we hand out a digest of this */
+    s = apr_psprintf(p, "{\"e\":\"%s\",\"kty\":\"RSA\",\"n\":\"%s\"}", e64, n64);
+    rv = md_crypt_sha256_digest64(pthumb, p, s, strlen(s));
+    return rv;
+}
diff --git a/modules/md/md_jws.h b/modules/md/md_jws.h
new file mode 100644 (file)
index 0000000..bd3a006
--- /dev/null
@@ -0,0 +1,29 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef mod_md_md_jws_h
+#define mod_md_md_jws_h
+
+struct apr_table_t;
+struct md_json_t;
+struct md_pkey_t;
+
+apr_status_t md_jws_sign(md_json_t **pmsg, apr_pool_t *p,
+                         const char *payload, size_t len, struct apr_table_t *protected, 
+                         struct md_pkey_t *pkey, const char *key_id);
+
+apr_status_t md_jws_pkey_thumb(const char **pthumb, apr_pool_t *p, struct md_pkey_t *pkey);
+
+#endif /* md_jws_h */
diff --git a/modules/md/md_log.c b/modules/md/md_log.c
new file mode 100644 (file)
index 0000000..c62b69d
--- /dev/null
@@ -0,0 +1,77 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_buckets.h>
+
+#include "md_log.h"
+
+#define LOG_BUFFER_LEN  1024
+
+static const char *level_names[] = {
+    "emergency",
+    "alert",
+    "crit",
+    "err",
+    "warning",
+    "notice",
+    "info",
+    "debug",
+    "trace1",
+    "trace2",
+    "trace3",
+    "trace4",
+    "trace5",
+    "trace6",
+    "trace7",
+    "trace8",
+};
+
+const char *md_log_level_name(md_log_level_t level)
+{
+    return level_names[level];
+}
+
+static md_log_print_cb *log_printv;
+static md_log_level_cb *log_level;
+static void *log_baton;
+
+void md_log_set(md_log_level_cb *level_cb, md_log_print_cb *print_cb, void *baton)
+{
+    log_printv = print_cb;
+    log_level = level_cb;
+    log_baton = baton;
+}
+
+int md_log_is_level(apr_pool_t *p, md_log_level_t level)
+{
+    if (!log_level) {
+        return 0;
+    }
+    return log_level(log_baton, p, level);
+}
+
+void md_log_perror(const char *file, int line, md_log_level_t level, 
+                   apr_status_t rv, apr_pool_t *p, const char *fmt, ...)
+{
+    va_list ap;
+
+    va_start(ap, fmt);
+    if (log_printv) {
+        log_printv(file, line, level, rv, log_baton, p, fmt, ap);
+    }
+    va_end(ap);
+}
diff --git a/modules/md/md_log.h b/modules/md/md_log.h
new file mode 100644 (file)
index 0000000..9af15a7
--- /dev/null
@@ -0,0 +1,55 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef mod_md_md_log_h
+#define mod_md_md_log_h
+
+typedef enum {
+    MD_LOG_EMERG,
+    MD_LOG_ALERT,
+    MD_LOG_CRIT,
+    MD_LOG_ERR, 
+    MD_LOG_WARNING, 
+    MD_LOG_NOTICE, 
+    MD_LOG_INFO, 
+    MD_LOG_DEBUG, 
+    MD_LOG_TRACE1, 
+    MD_LOG_TRACE2, 
+    MD_LOG_TRACE3, 
+    MD_LOG_TRACE4, 
+    MD_LOG_TRACE5, 
+    MD_LOG_TRACE6, 
+    MD_LOG_TRACE7, 
+    MD_LOG_TRACE8, 
+} md_log_level_t;
+
+#define MD_LOG_MARK     __FILE__,__LINE__
+
+const char *md_log_level_name(md_log_level_t level);
+
+int md_log_is_level(apr_pool_t *p, md_log_level_t level);
+
+void md_log_perror(const char *file, int line, md_log_level_t level, 
+                   apr_status_t rv, apr_pool_t *p, const char *fmt, ...);
+
+
+typedef int md_log_level_cb(void *baton, apr_pool_t *p, md_log_level_t level);
+
+typedef void md_log_print_cb(const char *file, int line, md_log_level_t level, 
+                apr_status_t rv, void *baton, apr_pool_t *p, const char *fmt, va_list ap);
+
+void md_log_set(md_log_level_cb *level_cb, md_log_print_cb *print_cb, void *baton);
+
+#endif /* md_log_h */
diff --git a/modules/md/md_reg.c b/modules/md/md_reg.c
new file mode 100644 (file)
index 0000000..ddb60ea
--- /dev/null
@@ -0,0 +1,986 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_hash.h>
+#include <apr_strings.h>
+#include <apr_uri.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_log.h"
+#include "md_json.h"
+#include "md_reg.h"
+#include "md_store.h"
+#include "md_util.h"
+
+#include "md_acme.h"
+#include "md_acme_acct.h"
+
+struct md_reg_t {
+    struct md_store_t *store;
+    struct apr_hash_t *protos;
+    int can_http;
+    int can_https;
+    const char *proxy_url;
+};
+
+/**************************************************************************************************/
+/* life cycle */
+
+static apr_status_t load_props(md_reg_t *reg, apr_pool_t *p)
+{
+    md_json_t *json;
+    apr_status_t rv;
+    
+    rv = md_store_load(reg->store, MD_SG_NONE, NULL, MD_FN_HTTPD_JSON, 
+                       MD_SV_JSON, (void**)&json, p);
+    if (APR_SUCCESS == rv) {
+        if (md_json_has_key(json, MD_KEY_PROTO, MD_KEY_HTTP, NULL)) {
+            reg->can_http = md_json_getb(json, MD_KEY_PROTO, MD_KEY_HTTP, NULL);
+        }
+        if (md_json_has_key(json, MD_KEY_PROTO, MD_KEY_HTTPS, NULL)) {
+            reg->can_https = md_json_getb(json, MD_KEY_PROTO, MD_KEY_HTTPS, NULL);
+        }
+    }
+    else if (APR_STATUS_IS_ENOENT(rv)) {
+        rv = APR_SUCCESS;
+    }
+    return rv;
+}
+
+apr_status_t md_reg_init(md_reg_t **preg, apr_pool_t *p, struct md_store_t *store,
+                         const char *proxy_url)
+{
+    md_reg_t *reg;
+    apr_status_t rv;
+    
+    reg = apr_pcalloc(p, sizeof(*reg));
+    reg->store = store;
+    reg->protos = apr_hash_make(p);
+    reg->can_http = 1;
+    reg->can_https = 1;
+    reg->proxy_url = proxy_url? apr_pstrdup(p, proxy_url) : NULL;
+    
+    if (APR_SUCCESS == (rv = md_acme_protos_add(reg->protos, p))) {
+        rv = load_props(reg, p);
+    }
+    
+    *preg = (rv == APR_SUCCESS)? reg : NULL;
+    return rv;
+}
+
+struct md_store_t *md_reg_store_get(md_reg_t *reg)
+{
+    return reg->store;
+}
+
+/**************************************************************************************************/
+/* checks */
+
+static apr_status_t check_values(md_reg_t *reg, apr_pool_t *p, const md_t *md, int fields)
+{
+    apr_status_t rv = APR_SUCCESS;
+    const char *err = NULL;
+    
+    if (MD_UPD_DOMAINS & fields) {
+        const md_t *other;
+        const char *domain;
+        int i;
+        
+        if (!md->domains || md->domains->nelts <= 0) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, APR_EINVAL, p, 
+                          "empty domain list: %s", md->name);
+            return APR_EINVAL;
+        }
+        
+        for (i = 0; i < md->domains->nelts; ++i) {
+            domain = APR_ARRAY_IDX(md->domains, i, const char *);
+            if (!md_util_is_dns_name(p, domain, 1)) {
+                md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p, 
+                              "md %s with invalid domain name: %s", md->name, domain);
+                return APR_EINVAL;
+            }
+        }
+
+        if (NULL != (other = md_reg_find_overlap(reg, md, &domain, p))) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p, 
+                          "md %s shares domain '%s' with md %s", 
+                          md->name, domain, other->name);
+            return APR_EINVAL;
+        }
+    }
+    
+    if (MD_UPD_CONTACTS & fields) {
+        const char *contact;
+        int i;
+
+        for (i = 0; i < md->contacts->nelts && !err; ++i) {
+            contact = APR_ARRAY_IDX(md->contacts, i, const char *);
+            rv = md_util_abs_uri_check(p, contact, &err);
+            
+            if (err) {
+                md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p, 
+                              "contact for %s invalid (%s): %s", md->name, err, contact);
+                return APR_EINVAL;
+            }
+        }
+    }
+    
+    if ((MD_UPD_CA_URL & fields) && md->ca_url) { /* setting to empty is ok */
+        rv = md_util_abs_uri_check(p, md->ca_url, &err);
+        if (err) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p, 
+                          "CA url for %s invalid (%s): %s", md->name, err, md->ca_url);
+            return APR_EINVAL;
+        }
+    }
+    
+    if ((MD_UPD_CA_PROTO & fields) && md->ca_proto) { /* setting to empty is ok */
+        /* Do we want to restrict this to "known" protocols? */
+    }
+    
+    if ((MD_UPD_CA_ACCOUNT & fields) && md->ca_account) { /* setting to empty is ok */
+        /* hmm, in case we know the protocol, some checks could be done */
+    }
+
+    if ((MD_UPD_AGREEMENT & fields) && md->ca_agreement) { /* setting to empty is ok */
+        rv = md_util_abs_uri_check(p, md->ca_agreement, &err);
+        if (err) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, p, 
+                          "CA url for %s invalid (%s): %s", md->name, err, md->ca_agreement);
+            return APR_EINVAL;
+        }
+    }
+
+    return rv;
+}
+
+/**************************************************************************************************/
+/* state assessment */
+
+static apr_status_t state_init(md_reg_t *reg, apr_pool_t *p, md_t *md, int save_changes)
+{
+    md_state_t state = MD_S_UNKNOWN;
+    const md_creds_t *creds;
+    const md_cert_t *cert;
+    apr_time_t expires = 0, valid_from = 0;
+    apr_status_t rv;
+    int i;
+
+    if (APR_SUCCESS == (rv = md_reg_creds_get(&creds, reg, MD_SG_DOMAINS, md, p))) {
+        state = MD_S_INCOMPLETE;
+        if (!creds->privkey) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, 
+                          "md{%s}: incomplete, without private key", md->name);
+        }
+        else if (!creds->cert) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, 
+                          "md{%s}: incomplete, has key but no certificate", md->name);
+        }
+        else {
+            valid_from = md_cert_get_not_before(creds->cert);
+            expires = md_cert_get_not_after(creds->cert);
+            if (md_cert_has_expired(creds->cert)) {
+                state = MD_S_EXPIRED;
+                md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, 
+                              "md{%s}: expired, certificate has expired", md->name);
+                goto out;
+            }
+            if (!md_cert_is_valid_now(creds->cert)) {
+                state = MD_S_ERROR;
+                md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, 
+                              "md{%s}: error, certificate valid in future (clock wrong?)", 
+                              md->name);
+                goto out;
+            }
+            if (!md_cert_covers_md(creds->cert, md)) {
+                state = MD_S_INCOMPLETE;
+                md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, p, 
+                              "md{%s}: incomplete, cert no longer covers all domains, "
+                              "needs sign up for a new certificate", md->name);
+                goto out;
+            }
+
+            for (i = 1; i < creds->pubcert->nelts; ++i) {
+                cert = APR_ARRAY_IDX(creds->pubcert, i, const md_cert_t *);
+                if (!md_cert_is_valid_now(cert)) {
+                    state = MD_S_ERROR;
+                    md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, 
+                                  "md{%s}: error, the certificate itself is valid, however the %d. "
+                                  "certificate in the chain is not valid now (clock wrong?).", 
+                                  md->name, i);
+                    goto out;
+                }
+            } 
+
+            state = MD_S_COMPLETE;
+            md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "md{%s}: is complete", md->name);
+        }
+    }
+
+out:    
+    if (APR_SUCCESS != rv) {
+        state = MD_S_ERROR;
+        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, "md{%s}: error", md->name);
+    }
+    
+    if (save_changes && md->state == state
+        && md->valid_from == valid_from && md->expires == expires) {
+        save_changes = 0;
+    }
+    md->state = state;
+    md->valid_from = valid_from;
+    md->expires = expires;
+    if (save_changes && APR_SUCCESS == rv) {
+        return md_save(reg->store, p, MD_SG_DOMAINS, md, 0);
+    }
+    return rv;
+}
+
+apr_status_t md_reg_assess(md_reg_t *reg, md_t *md, int *perrored, int *prenew, apr_pool_t *p)
+{
+    int renew = 0;
+    int errored = 0;
+    
+    (void)reg;
+    switch (md->state) {
+        case MD_S_UNKNOWN:
+            md_log_perror( MD_LOG_MARK, MD_LOG_ERR, 0, p, "md(%s): in unknown state.", md->name);
+            break;
+        case MD_S_ERROR:
+            md_log_perror( MD_LOG_MARK, MD_LOG_ERR, 0, p,  
+                         "md(%s): in error state, unable to drive forward. If unable to "
+                         " detect the cause, you may remove the staging or even domain "
+                         " sub-directory for this MD and start all over.", md->name);
+            errored = 1;
+            break;
+        case MD_S_COMPLETE:
+            if (!md->expires) {
+                md_log_perror( MD_LOG_MARK, MD_LOG_WARNING, 0, p,  
+                             "md(%s): looks complete, but has unknown expiration date.", md->name);
+                errored = 1;
+            }
+            else if (md->expires <= apr_time_now()) {
+                /* Maybe we hibernated in the meantime? */
+                md->state = MD_S_EXPIRED;
+                renew = 1;
+            }
+            else {
+                renew = md_should_renew(md);
+            }
+            break;
+        case MD_S_INCOMPLETE:
+        case MD_S_EXPIRED:
+            renew = 1;
+            break;
+        case MD_S_MISSING:
+            break;
+    }
+    *prenew = renew;
+    *perrored = errored;
+    return APR_SUCCESS;
+}
+
+/**************************************************************************************************/
+/* iteration */
+
+typedef struct {
+    md_reg_t *reg;
+    md_reg_do_cb *cb;
+    void *baton;
+    const char *exclude;
+    const void *result;
+} reg_do_ctx;
+
+static int reg_md_iter(void *baton, md_store_t *store, md_t *md, apr_pool_t *ptemp)
+{
+    reg_do_ctx *ctx = baton;
+    
+    (void)store;
+    if (!ctx->exclude || strcmp(ctx->exclude, md->name)) {
+        state_init(ctx->reg, ptemp, (md_t*)md, 1);
+        return ctx->cb(ctx->baton, ctx->reg, md);
+    }
+    return 1;
+}
+
+static int reg_do(md_reg_do_cb *cb, void *baton, md_reg_t *reg, apr_pool_t *p, const char *exclude)
+{
+    reg_do_ctx ctx;
+    
+    ctx.reg = reg;
+    ctx.cb = cb;
+    ctx.baton = baton;
+    ctx.exclude = exclude;
+    return md_store_md_iter(reg_md_iter, &ctx, reg->store, p, MD_SG_DOMAINS, "*");
+}
+
+
+int md_reg_do(md_reg_do_cb *cb, void *baton, md_reg_t *reg, apr_pool_t *p)
+{
+    return reg_do(cb, baton, reg, p, NULL);
+}
+
+/**************************************************************************************************/
+/* lookup */
+
+md_t *md_reg_get(md_reg_t *reg, const char *name, apr_pool_t *p)
+{
+    md_t *md;
+    
+    if (APR_SUCCESS == md_load(reg->store, MD_SG_DOMAINS, name, &md, p)) {
+        state_init(reg, p, md, 1);
+        return md;
+    }
+    return NULL;
+}
+
+typedef struct {
+    const char *domain;
+    md_t *md;
+} find_domain_ctx;
+
+static int find_domain(void *baton, md_reg_t *reg, md_t *md)
+{
+    find_domain_ctx *ctx = baton;
+    
+    (void)reg;
+    if (md_contains(md, ctx->domain, 0)) {
+        ctx->md = md;
+        return 0;
+    }
+    return 1;
+}
+
+md_t *md_reg_find(md_reg_t *reg, const char *domain, apr_pool_t *p)
+{
+    find_domain_ctx ctx;
+
+    ctx.domain = domain;
+    ctx.md = NULL;
+    
+    md_reg_do(find_domain, &ctx, reg, p);
+    if (ctx.md) {
+        state_init(reg, p, ctx.md, 1);
+    }
+    return ctx.md;
+}
+
+typedef struct {
+    const md_t *md_checked;
+    md_t *md;
+    const char *s;
+} find_overlap_ctx;
+
+static int find_overlap(void *baton, md_reg_t *reg, md_t *md)
+{
+    find_overlap_ctx *ctx = baton;
+    const char *overlap;
+    
+    (void)reg;
+    if ((overlap = md_common_name(ctx->md_checked, md))) {
+        ctx->md = md;
+        ctx->s = overlap;
+        return 0;
+    }
+    return 1;
+}
+
+md_t *md_reg_find_overlap(md_reg_t *reg, const md_t *md, const char **pdomain, apr_pool_t *p)
+{
+    find_overlap_ctx ctx;
+    
+    ctx.md_checked = md;
+    ctx.md = NULL;
+    ctx.s = NULL;
+    
+    reg_do(find_overlap, &ctx, reg, p, md->name);
+    if (pdomain && ctx.s) {
+        *pdomain = ctx.s;
+    }
+    if (ctx.md) {
+        state_init(reg, p, ctx.md, 1);
+    }
+    return ctx.md;
+}
+
+apr_status_t md_reg_get_cred_files(md_reg_t *reg, const md_t *md, apr_pool_t *p,
+                                   const char **pkeyfile, const char **pcertfile)
+{
+    apr_status_t rv;
+    
+    rv = md_store_get_fname(pkeyfile, reg->store, MD_SG_DOMAINS, md->name, MD_FN_PRIVKEY, p);
+    if (APR_SUCCESS == rv) {
+        rv = md_store_get_fname(pcertfile, reg->store, MD_SG_DOMAINS, md->name, MD_FN_PUBCERT, p);
+    }
+    return rv;
+}
+
+/**************************************************************************************************/
+/* manipulation */
+
+static apr_status_t p_md_add(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    md_reg_t *reg = baton;
+    apr_status_t rv = APR_SUCCESS;
+    md_t *md, *mine;
+    
+    md = va_arg(ap, md_t *);
+    mine = md_clone(ptemp, md);
+    if (APR_SUCCESS == (rv = check_values(reg, ptemp, md, MD_UPD_ALL))
+        && APR_SUCCESS == (rv = state_init(reg, ptemp, mine, 0))
+        && APR_SUCCESS == (rv = md_save(reg->store, p, MD_SG_DOMAINS, mine, 1))) {
+    }
+    return rv;
+}
+
+apr_status_t md_reg_add(md_reg_t *reg, md_t *md, apr_pool_t *p)
+{
+    return md_util_pool_vdo(p_md_add, reg, p, md, NULL);
+}
+
+static apr_status_t p_md_update(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    md_reg_t *reg = baton;
+    apr_status_t rv = APR_SUCCESS;
+    const char *name;
+    const md_t *md, *updates;
+    int fields;
+    md_t *nmd;
+    
+    name = va_arg(ap, const char *);
+    updates = va_arg(ap, const md_t *);
+    fields = va_arg(ap, int);
+    
+    if (NULL == (md = md_reg_get(reg, name, ptemp))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, APR_ENOENT, ptemp, "md %s", name);
+        return APR_ENOENT;
+    }
+    
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "update md %s", name);
+    
+    if (APR_SUCCESS != (rv = check_values(reg, ptemp, updates, fields))) {
+        return rv;
+    }
+    
+    nmd = md_copy(ptemp, md);
+    if (MD_UPD_DOMAINS & fields) {
+        nmd->domains = updates->domains;
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update domains: %s", name);
+    }
+    if (MD_UPD_CA_URL & fields) {
+        nmd->ca_url = updates->ca_url;
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update ca url: %s", name);
+    }
+    if (MD_UPD_CA_PROTO & fields) {
+        nmd->ca_proto = updates->ca_proto;
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update ca protocol: %s", name);
+    }
+    if (MD_UPD_CA_ACCOUNT & fields) {
+        nmd->ca_account = updates->ca_account;
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update account: %s", name);
+    }
+    if (MD_UPD_CONTACTS & fields) {
+        nmd->contacts = updates->contacts;
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update contacts: %s", name);
+    }
+    if (MD_UPD_AGREEMENT & fields) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update agreement: %s", name);
+        nmd->ca_agreement = updates->ca_agreement;
+    }
+    if (MD_UPD_CERT_URL & fields) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update cert url: %s", name);
+        nmd->cert_url = updates->cert_url;
+    }
+    if (MD_UPD_DRIVE_MODE & fields) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update drive-mode: %s", name);
+        nmd->drive_mode = updates->drive_mode;
+    }
+    if (MD_UPD_RENEW_WINDOW & fields) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update renew-window: %s", name);
+        nmd->renew_norm = updates->renew_norm;
+        nmd->renew_window = updates->renew_window;
+    }
+    if (MD_UPD_CA_CHALLENGES & fields) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update ca challenges: %s", name);
+        nmd->ca_challenges = (updates->ca_challenges? 
+                              apr_array_copy(p, updates->ca_challenges) : NULL);
+    }
+    if (MD_UPD_PKEY_SPEC & fields) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update pkey spec: %s", name);
+        nmd->pkey_spec = NULL;
+        if (updates->pkey_spec) {
+            nmd->pkey_spec = apr_pmemdup(p, updates->pkey_spec, sizeof(md_pkey_spec_t));
+        }
+    }
+    if (MD_UPD_REQUIRE_HTTPS & fields) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update require-https: %s", name);
+        nmd->require_https = updates->require_https;
+    }
+    if (MD_UPD_TRANSITIVE & fields) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update transitive: %s", name);
+        nmd->transitive = updates->transitive;
+    }
+    if (MD_UPD_MUST_STAPLE & fields) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE1, 0, ptemp, "update must-staple: %s", name);
+        nmd->must_staple = updates->must_staple;
+    }
+    
+    if (fields && APR_SUCCESS == (rv = md_save(reg->store, p, MD_SG_DOMAINS, nmd, 0))) {
+        rv = state_init(reg, ptemp, nmd, 0);
+    }
+    return rv;
+}
+
+apr_status_t md_reg_update(md_reg_t *reg, apr_pool_t *p, 
+                           const char *name, const md_t *md, int fields)
+{
+    return md_util_pool_vdo(p_md_update, reg, p, name, md, fields, NULL);
+}
+
+/**************************************************************************************************/
+/* certificate related */
+
+static int ok_or_noent(apr_status_t rv) 
+{
+    return (APR_SUCCESS == rv || APR_ENOENT == rv);
+}
+
+static apr_status_t creds_load(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    md_reg_t *reg = baton;
+    md_pkey_t *privkey;
+    apr_array_header_t *pubcert;
+    md_creds_t *creds, **pcreds;
+    const md_t *md;
+    md_cert_state_t cert_state;
+    md_store_group_t group;
+    apr_status_t rv;
+    
+    pcreds = va_arg(ap, md_creds_t **);
+    group = (md_store_group_t)va_arg(ap, int);
+    md = va_arg(ap, const md_t *);
+    
+    if (ok_or_noent(rv = md_pkey_load(reg->store, group, md->name, &privkey, p))
+        && ok_or_noent(rv = md_pubcert_load(reg->store, group, md->name, &pubcert, p))) {
+        rv = APR_SUCCESS;
+            
+        creds = apr_pcalloc(p, sizeof(*creds));
+        creds->privkey = privkey;
+        if (pubcert && pubcert->nelts > 0) {
+            creds->pubcert = pubcert;
+            creds->cert = APR_ARRAY_IDX(pubcert, 0, md_cert_t *);
+        }
+        if (creds->cert) {
+            switch ((cert_state = md_cert_state_get(creds->cert))) {
+                case MD_CERT_VALID:
+                    creds->expired = 0;
+                    break;
+                case MD_CERT_EXPIRED:
+                    creds->expired = 1;
+                    break;
+                default:
+                    md_log_perror(MD_LOG_MARK, MD_LOG_ERR, APR_EINVAL, ptemp, 
+                                  "md %s has unexpected cert state: %d", md->name, cert_state);
+                    rv = APR_ENOTIMPL;
+                    break;
+            }
+        }
+    }
+    *pcreds = (APR_SUCCESS == rv)? creds : NULL;
+    return rv;
+}
+
+apr_status_t md_reg_creds_get(const md_creds_t **pcreds, md_reg_t *reg, 
+                              md_store_group_t group, const md_t *md, apr_pool_t *p)
+{
+    apr_status_t rv = APR_SUCCESS;
+    md_creds_t *creds;
+    
+    rv = md_util_pool_vdo(creds_load, reg, p, &creds, group, md, NULL);
+    *pcreds = (APR_SUCCESS == rv)? creds : NULL;
+    return rv;
+}
+
+/**************************************************************************************************/
+/* synching */
+
+typedef struct {
+    apr_pool_t *p;
+    apr_array_header_t *conf_mds;
+    apr_array_header_t *store_mds;
+} sync_ctx;
+
+static int find_changes(void *baton, md_store_t *store, md_t *md, apr_pool_t *ptemp)
+{
+    sync_ctx *ctx = baton;
+
+    (void)store;
+    (void)ptemp;
+    APR_ARRAY_PUSH(ctx->store_mds, const md_t*) = md_clone(ctx->p, md);
+    return 1;
+}
+
+apr_status_t md_reg_set_props(md_reg_t *reg, apr_pool_t *p, int can_http, int can_https)
+{
+    if (reg->can_http != can_http || reg->can_https != can_https) {
+        md_json_t *json;
+        
+        reg->can_http = can_http;
+        reg->can_https = can_https;
+        
+        json = md_json_create(p);
+        md_json_setb(can_http, json, MD_KEY_PROTO, MD_KEY_HTTP, NULL);
+        md_json_setb(can_https, json, MD_KEY_PROTO, MD_KEY_HTTPS, NULL);
+        
+        return md_store_save(reg->store, p, MD_SG_NONE, NULL, MD_FN_HTTPD_JSON, MD_SV_JSON, json, 0);
+    }
+    return APR_SUCCESS;
+}
+/**
+ * Procedure:
+ * 1. Collect all defined "managed domains" (MD). It does not matter where a MD is defined. 
+ *    All MDs need to be unique and have no overlaps in their domain names. 
+ *    Fail the config otherwise. Also, if a vhost matches an MD, it
+ *    needs to *only* have ServerAliases from that MD. There can be no more than one
+ *    matching MD for a vhost. But an MD can apply to several vhosts.
+ * 2. Synchronize with the persistent store. Iterate over all configured MDs and 
+ *   a. create them in the store if they do not already exist, neither under the
+ *      name or with a common domain.
+ *   b. compare domain lists from store and config, if
+ *      - store has dns name in other MD than from config, remove dns name from store def,
+ *        issue WARNING.
+ *      - store misses dns name from config, add dns name and update store
+ *   c. compare MD acme url/protocol, update if changed
+ */
+apr_status_t md_reg_sync(md_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp, 
+                         apr_array_header_t *master_mds) 
+{
+    sync_ctx ctx;
+    md_store_t *store = reg->store;
+    apr_status_t rv;
+
+    ctx.p = ptemp;
+    ctx.conf_mds = master_mds;
+    ctx.store_mds = apr_array_make(ptemp, 100, sizeof(md_t *));
+    
+    rv = md_store_md_iter(find_changes, &ctx, store, ptemp, MD_SG_DOMAINS, "*");
+    if (APR_STATUS_IS_ENOENT(rv)) {
+        rv = APR_SUCCESS;
+    }
+    
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, 
+                  "sync: found %d mds in store", ctx.store_mds->nelts);
+    if (APR_SUCCESS == rv) {
+        int i, fields;
+        md_t *md, *config_md, *smd, *omd;
+        const char *common;
+        
+        for (i = 0; i < ctx.conf_mds->nelts; ++i) {
+            md = APR_ARRAY_IDX(ctx.conf_mds, i, md_t *);
+            
+            /* find the store md that is closest match for the configured md */
+            smd = md_find_closest_match(ctx.store_mds, md);
+            if (smd) {
+                fields = 0;
+                
+                /* Once stored, we keep the name */
+                if (strcmp(md->name, smd->name)) {
+                    md->name = apr_pstrdup(p, smd->name);
+                }
+                
+                /* Make the stored domain list *exactly* the same, even if
+                 * someone only changed upper/lowercase, we'd like to persist that. */
+                if (!md_equal_domains(md, smd, 1)) {
+                    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, 
+                                 "%s: %d domains changed", smd->name);
+                    smd->domains = md_array_str_clone(ptemp, md->domains);
+                    fields |= MD_UPD_DOMAINS;
+                }
+                
+                /* Look for other store mds which have domains now being part of smd */
+                while (APR_SUCCESS == rv && (omd = md_get_by_dns_overlap(ctx.store_mds, md))) {
+                    /* find the name now duplicate */
+                    common = md_common_name(md, omd);
+                    assert(common);
+                    
+                    /* Is this md still configured or has it been abandoned in the config? */
+                    config_md = md_get_by_name(ctx.conf_mds, omd->name);
+                    if (config_md && md_contains(config_md, common, 0)) {
+                        /* domain used in two configured mds, not allowed */
+                        rv = APR_EINVAL;
+                        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, 
+                                      "domain %s used in md %s and %s", 
+                                      common, md->name, omd->name);
+                    }
+                    else if (config_md) {
+                        /* domain stored in omd, but no longer has the offending domain,
+                           remove it from the store md. */
+                        omd->domains = md_array_str_remove(ptemp, omd->domains, common, 0);
+                        rv = md_reg_update(reg, ptemp, omd->name, omd, MD_UPD_DOMAINS);
+                    }
+                    else {
+                        /* domain in a store md that is no longer configured, warn about it.
+                         * Remove the domain here, so we can progress, but never save it. */
+                        omd->domains = md_array_str_remove(ptemp, omd->domains, common, 0);
+                        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, 
+                                      "domain %s, configured in md %s, is part of the stored md %s."
+                                      " That md however is no longer mentioned in the config. "
+                                      "If you longer want it, remove the md from the store.", 
+                                      common, md->name, omd->name);
+                    }
+                }
+
+                if (MD_SVAL_UPDATE(md, smd, ca_url)) {
+                    smd->ca_url = md->ca_url;
+                    fields |= MD_UPD_CA_URL;
+                }
+                if (MD_SVAL_UPDATE(md, smd, ca_proto)) {
+                    smd->ca_proto = md->ca_proto;
+                    fields |= MD_UPD_CA_PROTO;
+                }
+                if (MD_SVAL_UPDATE(md, smd, ca_agreement)) {
+                    smd->ca_agreement = md->ca_agreement;
+                    fields |= MD_UPD_AGREEMENT;
+                }
+                if (MD_VAL_UPDATE(md, smd, transitive)) {
+                    smd->transitive = md->transitive;
+                    fields |= MD_UPD_TRANSITIVE;
+                }
+                if (MD_VAL_UPDATE(md, smd, drive_mode)) {
+                    smd->drive_mode = md->drive_mode;
+                    fields |= MD_UPD_DRIVE_MODE;
+                }
+                if (!apr_is_empty_array(md->contacts) 
+                    && !md_array_str_eq(md->contacts, smd->contacts, 0)) {
+                    smd->contacts = md->contacts;
+                    fields |= MD_UPD_CONTACTS;
+                }
+                if (MD_VAL_UPDATE(md, smd, renew_window) 
+                    || MD_VAL_UPDATE(md, smd, renew_norm)) {
+                    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, 
+                                  "%s: update renew norm=%ld, window=%ld", 
+                                  smd->name, (long)md->renew_norm, (long)md->renew_window);
+                    smd->renew_norm = md->renew_norm;
+                    smd->renew_window = md->renew_window;
+                    fields |= MD_UPD_RENEW_WINDOW;
+                }
+                if (md->ca_challenges) {
+                    md->ca_challenges = md_array_str_compact(p, md->ca_challenges, 0);
+                    if (!smd->ca_challenges 
+                        || !md_array_str_eq(md->ca_challenges, smd->ca_challenges, 0)) {
+                        smd->ca_challenges = apr_array_copy(ptemp, md->ca_challenges);
+                        fields |= MD_UPD_CA_CHALLENGES;
+                    }
+                }
+                else if (smd->ca_challenges) {
+                    smd->ca_challenges = NULL;
+                    fields |= MD_UPD_CA_CHALLENGES;
+                }
+                if (!md_pkey_spec_eq(md->pkey_spec, smd->pkey_spec)) {
+                    fields |= MD_UPD_PKEY_SPEC;
+                    smd->pkey_spec = NULL;
+                    if (md->pkey_spec) {
+                        smd->pkey_spec = apr_pmemdup(p, md->pkey_spec, sizeof(md_pkey_spec_t));
+                    }
+                }
+                if (MD_VAL_UPDATE(md, smd, require_https)) {
+                    smd->require_https = md->require_https;
+                    fields |= MD_UPD_REQUIRE_HTTPS;
+                }
+                if (MD_VAL_UPDATE(md, smd, must_staple)) {
+                    smd->must_staple = md->must_staple;
+                    fields |= MD_UPD_MUST_STAPLE;
+                }
+                
+                if (fields) {
+                    rv = md_reg_update(reg, ptemp, smd->name, smd, fields);
+                    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "md %s updated", smd->name);
+                }
+            }
+            else {
+                /* new managed domain */
+                rv = md_reg_add(reg, md, ptemp);
+                md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, p, "new md %s added", md->name);
+            }
+        }
+    }
+    else {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "loading mds");
+    }
+    
+    return rv;
+}
+
+
+/**************************************************************************************************/
+/* driving */
+
+static apr_status_t init_proto_driver(md_proto_driver_t *driver, const md_proto_t *proto, 
+                                      md_reg_t *reg, const md_t *md, 
+                                      const char *challenge, int reset, apr_pool_t *p) 
+{
+    apr_status_t rv = APR_SUCCESS;
+
+    /* If this registry instance was not synched before (and obtained server
+     * properties that way), read them from the store.
+     */
+    driver->proto = proto;
+    driver->p = p;
+    driver->challenge = challenge;
+    driver->can_http = reg->can_http;
+    driver->can_https = reg->can_https;
+    driver->reg = reg;
+    driver->store = md_reg_store_get(reg);
+    driver->proxy_url = reg->proxy_url;
+    driver->md = md;
+    driver->reset = reset;
+
+    return rv;
+}
+
+static apr_status_t run_stage(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    md_reg_t *reg = baton;
+    const md_proto_t *proto;
+    const md_t *md;
+    int reset;
+    md_proto_driver_t *driver;
+    const char *challenge;
+    apr_time_t *pvalid_from;
+    apr_status_t rv;
+    
+    (void)p;
+    proto = va_arg(ap, const md_proto_t *);
+    md = va_arg(ap, const md_t *);
+    challenge = va_arg(ap, const char *);
+    reset = va_arg(ap, int); 
+    pvalid_from = va_arg(ap, apr_time_t*);
+    
+    driver = apr_pcalloc(ptemp, sizeof(*driver));
+    rv = init_proto_driver(driver, proto, reg, md, challenge, reset, ptemp);
+    if (APR_SUCCESS == rv && 
+        APR_SUCCESS == (rv = proto->init(driver))) {
+        
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "%s: run staging", md->name);
+        rv = proto->stage(driver);
+
+        if (APR_SUCCESS == rv && pvalid_from) {
+            *pvalid_from = driver->stage_valid_from;
+        }
+    }
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "%s: staging done", md->name);
+    return rv;
+}
+
+apr_status_t md_reg_stage(md_reg_t *reg, const md_t *md, const char *challenge, 
+                          int reset, apr_time_t *pvalid_from, apr_pool_t *p)
+{
+    const md_proto_t *proto;
+    
+    if (!md->ca_proto) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, "md %s has no CA protocol", md->name);
+        ((md_t *)md)->state = MD_S_ERROR;
+        return APR_SUCCESS;
+    }
+    
+    proto = apr_hash_get(reg->protos, md->ca_proto, (apr_ssize_t)strlen(md->ca_proto));
+    if (!proto) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, 
+                      "md %s has unknown CA protocol: %s", md->name, md->ca_proto);
+        ((md_t *)md)->state = MD_S_ERROR;
+        return APR_EINVAL;
+    }
+    
+    return md_util_pool_vdo(run_stage, reg, p, proto, md, challenge, reset, pvalid_from, NULL);
+}
+
+static apr_status_t run_load(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    md_reg_t *reg = baton;
+    const char *name;
+    const md_proto_t *proto;
+    const md_t *md, *nmd;
+    md_proto_driver_t *driver;
+    apr_status_t rv;
+    
+    name = va_arg(ap, const char *);
+    
+    if (APR_STATUS_IS_ENOENT(rv = md_load(reg->store, MD_SG_STAGING, name, NULL, ptemp))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, ptemp, "%s: nothing staged", name);
+        return APR_ENOENT;
+    }
+    
+    md = md_reg_get(reg, name, p);
+    if (!md) {
+        return APR_ENOENT;
+    }
+    
+    if (!md->ca_proto) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, "md %s has no CA protocol", name);
+        ((md_t *)md)->state = MD_S_ERROR;
+        return APR_EINVAL;
+    }
+    
+    proto = apr_hash_get(reg->protos, md->ca_proto, (apr_ssize_t)strlen(md->ca_proto));
+    if (!proto) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, 0, p, 
+                      "md %s has unknown CA protocol: %s", md->name, md->ca_proto);
+        ((md_t *)md)->state = MD_S_ERROR;
+        return APR_EINVAL;
+    }
+    
+    driver = apr_pcalloc(ptemp, sizeof(*driver));
+    init_proto_driver(driver, proto, reg, md, NULL, 0, ptemp);
+
+    if (APR_SUCCESS == (rv = proto->init(driver))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "%s: run load", md->name);
+        
+        if (APR_SUCCESS == (rv = proto->preload(driver, MD_SG_TMP))) {
+            /* swap */
+            rv = md_store_move(reg->store, p, MD_SG_TMP, MD_SG_DOMAINS, md->name, 1);
+            if (APR_SUCCESS == rv) {
+                /* load again */
+                nmd = md_reg_get(reg, md->name, p);
+                if (!nmd) {
+                    rv = APR_ENOENT;
+                    md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "loading md after staging");
+                }
+                else if (nmd->state != MD_S_COMPLETE) {
+                    md_log_perror(MD_LOG_MARK, MD_LOG_WARNING, rv, p, 
+                                  "md has state %d after load", nmd->state);
+                }
+                
+                md_store_purge(reg->store, p, MD_SG_STAGING, md->name);
+                md_store_purge(reg->store, p, MD_SG_CHALLENGES, md->name);
+            }
+        }
+    }
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "%s: load done", md->name);
+    return rv;
+}
+
+apr_status_t md_reg_load(md_reg_t *reg, const char *name, apr_pool_t *p)
+{
+    return md_util_pool_vdo(run_load, reg, p, name, NULL);
+}
+
diff --git a/modules/md/md_reg.h b/modules/md/md_reg.h
new file mode 100644 (file)
index 0000000..74a831f
--- /dev/null
@@ -0,0 +1,176 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef mod_md_md_reg_h
+#define mod_md_md_reg_h
+
+struct apr_hash_t;
+struct apr_array_header_t;
+struct md_store_t;
+struct md_pkey_t;
+struct md_cert_t;
+
+/**
+ * A registry for managed domains with a md_store_t as persistence.
+ *
+ */
+typedef struct md_reg_t md_reg_t;
+
+/**
+ * Initialize the registry, using the pool and loading any existing information
+ * from the store.
+ */
+apr_status_t md_reg_init(md_reg_t **preg, apr_pool_t *pm, struct md_store_t *store,
+                         const char *proxy_url);
+
+struct md_store_t *md_reg_store_get(md_reg_t *reg);
+
+apr_status_t md_reg_set_props(md_reg_t *reg, apr_pool_t *p, int can_http, int can_https);
+
+/**
+ * Add a new md to the registry. This will check the name for uniqueness and
+ * that domain names do not overlap with already existing mds.
+ */
+apr_status_t md_reg_add(md_reg_t *reg, md_t *md, apr_pool_t *p);
+
+/**
+ * Find the md, if any, that contains the given domain name. 
+ * NULL if none found.
+ */
+md_t *md_reg_find(md_reg_t *reg, const char *domain, apr_pool_t *p);
+
+/**
+ * Find one md, which domain names overlap with the given md and that has a different
+ * name. There may be more than one existing md that overlaps. It is not defined
+ * which one will be returned. 
+ */
+md_t *md_reg_find_overlap(md_reg_t *reg, const md_t *md, const char **pdomain, apr_pool_t *p);
+
+/**
+ * Get the md with the given unique name. NULL if it does not exist.
+ * Will update the md->state.
+ */
+md_t *md_reg_get(md_reg_t *reg, const char *name, apr_pool_t *p);
+
+/**
+ * Assess the capability and need to driving this managed domain.
+ */
+apr_status_t md_reg_assess(md_reg_t *reg, md_t *md, int *perrored, int *prenew, apr_pool_t *p);
+
+/**
+ * Callback invoked for every md in the registry. If 0 is returned, iteration stops.
+ */
+typedef int md_reg_do_cb(void *baton, md_reg_t *reg, md_t *md);
+
+/**
+ * Invoke callback for all mds in this registry. Order is not guaranteed.
+ * If the callback returns 0, iteration stops. Returns 0 if iteration was
+ * aborted.
+ */
+int md_reg_do(md_reg_do_cb *cb, void *baton, md_reg_t *reg, apr_pool_t *p);
+
+/**
+ * Bitmask for fields that are updated.
+ */
+#define MD_UPD_DOMAINS       0x0001
+#define MD_UPD_CA_URL        0x0002
+#define MD_UPD_CA_PROTO      0x0004
+#define MD_UPD_CA_ACCOUNT    0x0008
+#define MD_UPD_CONTACTS      0x0010
+#define MD_UPD_AGREEMENT     0x0020
+#define MD_UPD_CERT_URL      0x0040
+#define MD_UPD_DRIVE_MODE    0x0080
+#define MD_UPD_RENEW_WINDOW  0x0100
+#define MD_UPD_CA_CHALLENGES 0x0200
+#define MD_UPD_PKEY_SPEC     0x0400
+#define MD_UPD_REQUIRE_HTTPS 0x0800
+#define MD_UPD_TRANSITIVE    0x1000
+#define MD_UPD_MUST_STAPLE   0x2000
+#define MD_UPD_ALL           0x7FFFFFFF
+
+/**
+ * Update the given fields for the managed domain. Take the new
+ * values from the given md, all other values remain unchanged.
+ */
+apr_status_t md_reg_update(md_reg_t *reg, apr_pool_t *p, 
+                           const char *name, const md_t *md, int fields);
+
+/**
+ * Get the credentials available for the managed domain md. Returns APR_ENOENT
+ * when none is available. The returned values are immutable. 
+ */
+apr_status_t md_reg_creds_get(const md_creds_t **pcreds, md_reg_t *reg, 
+                              md_store_group_t group, const md_t *md, apr_pool_t *p);
+
+apr_status_t md_reg_get_cred_files(md_reg_t *reg, const md_t *md, apr_pool_t *p,
+                                   const char **pkeyfile, const char **pcertfile);
+
+/**
+ * Synchronise the give master mds with the store.
+ */
+apr_status_t md_reg_sync(md_reg_t *reg, apr_pool_t *p, apr_pool_t *ptemp, 
+                         apr_array_header_t *master_mds);
+
+/**************************************************************************************************/
+/* protocol drivers */
+
+typedef struct md_proto_t md_proto_t;
+
+typedef struct md_proto_driver_t md_proto_driver_t;
+
+struct md_proto_driver_t {
+    const md_proto_t *proto;
+    apr_pool_t *p;
+    const char *challenge;
+    int can_http;
+    int can_https;
+    struct md_store_t *store;
+    md_reg_t *reg;
+    const md_t *md;
+    void *baton;
+    int reset;
+    apr_time_t stage_valid_from;
+    const char *proxy_url;
+};
+
+typedef apr_status_t md_proto_init_cb(md_proto_driver_t *driver);
+typedef apr_status_t md_proto_stage_cb(md_proto_driver_t *driver);
+typedef apr_status_t md_proto_preload_cb(md_proto_driver_t *driver, md_store_group_t group);
+
+struct md_proto_t {
+    const char *protocol;
+    md_proto_init_cb *init;
+    md_proto_stage_cb *stage;
+    md_proto_preload_cb *preload;
+};
+
+
+/**
+ * Stage a new credentials set for the given managed domain in a separate location
+ * without interfering with any existing credentials.
+ */
+apr_status_t md_reg_stage(md_reg_t *reg, const md_t *md, 
+                          const char *challenge, int reset, 
+                          apr_time_t *pvalid_from, apr_pool_t *p);
+
+/**
+ * Load a staged set of new credentials for the managed domain. This will archive
+ * any existing credential data and make the staged set the new live one.
+ * If staging is incomplete or missing, the load will fail and all credentials remain
+ * as they are.
+ */
+apr_status_t md_reg_load(md_reg_t *reg, const char *name, apr_pool_t *p);
+
+#endif /* mod_md_md_reg_h */
diff --git a/modules/md/md_store.c b/modules/md/md_store.c
new file mode 100644 (file)
index 0000000..801d69e
--- /dev/null
@@ -0,0 +1,318 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_file_info.h>
+#include <apr_file_io.h>
+#include <apr_fnmatch.h>
+#include <apr_hash.h>
+#include <apr_strings.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_log.h"
+#include "md_json.h"
+#include "md_store.h"
+#include "md_util.h"
+
+/**************************************************************************************************/
+/* generic callback handling */
+
+#define ASPECT_MD           "md.json"
+#define ASPECT_CERT         "cert.pem"
+#define ASPECT_PKEY         "key.pem"
+#define ASPECT_CHAIN        "chain.pem"
+
+#define GNAME_ACCOUNTS     
+#define GNAME_CHALLENGES   
+#define GNAME_DOMAINS      
+#define GNAME_STAGING      
+#define GNAME_ARCHIVE      
+
+static const char *GROUP_NAME[] = {
+    "none",
+    "accounts",
+    "challenges",
+    "domains",
+    "staging",
+    "archive",
+    "tmp",
+    NULL
+};
+
+const char *md_store_group_name(int group)
+{
+    if ((size_t)group < sizeof(GROUP_NAME)/sizeof(GROUP_NAME[0])) {
+        return GROUP_NAME[group];
+    }
+    return "UNKNOWN";
+}
+
+void md_store_destroy(md_store_t *store)
+{
+    if (store->destroy) store->destroy(store);
+}
+
+apr_status_t md_store_load(md_store_t *store, md_store_group_t group, 
+                           const char *name, const char *aspect, 
+                           md_store_vtype_t vtype, void **pdata, 
+                           apr_pool_t *p)
+{
+    return store->load(store, group, name, aspect, vtype, pdata, p);
+}
+
+apr_status_t md_store_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, 
+                           const char *name, const char *aspect, 
+                           md_store_vtype_t vtype, void *data, 
+                           int create)
+{
+    return store->save(store, p, group, name, aspect, vtype, data, create);
+}
+
+apr_status_t md_store_remove(md_store_t *store, md_store_group_t group, 
+                             const char *name, const char *aspect, 
+                             apr_pool_t *p, int force)
+{
+    return store->remove(store, group, name, aspect, p, force);
+}
+
+apr_status_t md_store_purge(md_store_t *store, apr_pool_t *p, md_store_group_t group, 
+                             const char *name)
+{
+    return store->purge(store, p, group, name);
+}
+
+apr_status_t md_store_iter(md_store_inspect *inspect, void *baton, md_store_t *store, 
+                           apr_pool_t *p, md_store_group_t group, const char *pattern, 
+                           const char *aspect, md_store_vtype_t vtype)
+{
+    return store->iterate(inspect, baton, store, p, group, pattern, aspect, vtype);
+}
+
+apr_status_t md_store_load_json(md_store_t *store, md_store_group_t group, 
+                                const char *name, const char *aspect, 
+                                struct md_json_t **pdata, apr_pool_t *p)
+{
+    return md_store_load(store, group, name, aspect, MD_SV_JSON, (void**)pdata, p);
+}
+
+apr_status_t md_store_save_json(md_store_t *store, apr_pool_t *p, md_store_group_t group, 
+                                const char *name, const char *aspect, 
+                                struct md_json_t *data, int create)
+{
+    return md_store_save(store, p, group, name, aspect, MD_SV_JSON, (void*)data, create);
+}
+
+apr_status_t md_store_move(md_store_t *store, apr_pool_t *p, 
+                           md_store_group_t from, md_store_group_t to,
+                           const char *name, int archive)
+{
+    return store->move(store, p, from, to, name, archive);
+}
+
+apr_status_t md_store_get_fname(const char **pfname, 
+                                md_store_t *store, md_store_group_t group, 
+                                const char *name, const char *aspect, 
+                                apr_pool_t *p)
+{
+    if (store->get_fname) {
+        return store->get_fname(pfname, store, group, name, aspect, p);
+    }
+    return APR_ENOTIMPL;
+}
+
+int md_store_is_newer(md_store_t *store, md_store_group_t group1, md_store_group_t group2,  
+                      const char *name, const char *aspect, apr_pool_t *p)
+{
+    return store->is_newer(store, group1, group2, name, aspect, p);
+}
+
+/**************************************************************************************************/
+/* convenience */
+
+typedef struct {
+    md_store_t *store;
+    md_store_group_t group;
+} md_group_ctx;
+
+apr_status_t md_load(md_store_t *store, md_store_group_t group, 
+                     const char *name, md_t **pmd, apr_pool_t *p)
+{
+    md_json_t *json;
+    apr_status_t rv;
+    
+    rv = md_store_load_json(store, group, name, MD_FN_MD, pmd? &json : NULL, p);
+    if (APR_SUCCESS == rv) {
+        if (pmd) {
+            *pmd = md_from_json(json, p);
+        }
+        return APR_SUCCESS;
+    }
+    return rv;
+}
+
+static apr_status_t p_save(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    md_group_ctx *ctx = baton;
+    md_json_t *json;
+    md_t *md;
+    int create;
+    
+    md = va_arg(ap, md_t *);
+    create = va_arg(ap, int);
+
+    json = md_to_json(md, ptemp);
+    assert(json);
+    assert(md->name);
+    return md_store_save_json(ctx->store, p, ctx->group, md->name, MD_FN_MD, json, create);
+}
+
+apr_status_t md_save(md_store_t *store, apr_pool_t *p, 
+                     md_store_group_t group, md_t *md, int create)
+{
+    md_group_ctx ctx;
+    
+    ctx.store = store;
+    ctx.group = group;
+    return md_util_pool_vdo(p_save, &ctx, p, md, create, NULL);
+}
+
+static apr_status_t p_remove(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    md_group_ctx *ctx = baton;
+    const char *name;
+    int force;
+    
+    (void)p;
+    name = va_arg(ap, const char *);
+    force = va_arg(ap, int);
+
+    assert(name);
+    return md_store_remove(ctx->store, ctx->group, name, MD_FN_MD, ptemp, force);
+}
+
+apr_status_t md_remove(md_store_t *store, apr_pool_t *p, 
+                       md_store_group_t group, const char *name, int force)
+{
+    md_group_ctx ctx;
+    
+    ctx.store = store;
+    ctx.group = group;
+    return md_util_pool_vdo(p_remove, &ctx, p, name, force, NULL);
+}
+
+int md_is_newer(md_store_t *store, md_store_group_t group1, md_store_group_t group2,  
+                      const char *name, apr_pool_t *p)
+{
+    return md_store_is_newer(store, group1, group2, name, MD_FN_MD, p);
+}
+
+
+typedef struct {
+    apr_pool_t *p;
+    apr_array_header_t *mds;
+} md_load_ctx;
+
+apr_status_t md_pkey_load(md_store_t *store, md_store_group_t group, const char *name, 
+                          md_pkey_t **ppkey, apr_pool_t *p)
+{
+    return md_store_load(store, group, name, MD_FN_PRIVKEY, MD_SV_PKEY, (void**)ppkey, p);
+}
+
+apr_status_t md_pkey_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, const char *name, 
+                          struct md_pkey_t *pkey, int create)
+{
+    return md_store_save(store, p, group, name, MD_FN_PRIVKEY, MD_SV_PKEY, pkey, create);
+}
+
+apr_status_t md_cert_load(md_store_t *store, md_store_group_t group, const char *name, 
+                          struct md_cert_t **pcert, apr_pool_t *p)
+{
+    return md_store_load(store, group, name, MD_FN_CERT, MD_SV_CERT, (void**)pcert, p);
+}
+
+apr_status_t md_cert_save(md_store_t *store, apr_pool_t *p, 
+                          md_store_group_t group, const char *name, 
+                          struct md_cert_t *cert, int create)
+{
+    return md_store_save(store, p, group, name, MD_FN_CERT, MD_SV_CERT, cert, create);
+}
+
+apr_status_t md_chain_load(md_store_t *store, md_store_group_t group, const char *name, 
+                           struct apr_array_header_t **pchain, apr_pool_t *p)
+{
+    return md_store_load(store, group, name, MD_FN_CHAIN, MD_SV_CHAIN, (void**)pchain, p);
+}
+
+apr_status_t md_chain_save(md_store_t *store, apr_pool_t *p, 
+                           md_store_group_t group, const char *name, 
+                           struct apr_array_header_t *chain, int create)
+{
+    return md_store_save(store, p, group, name, MD_FN_CHAIN, MD_SV_CHAIN, chain, create);
+}
+
+apr_status_t md_pubcert_load(md_store_t *store, md_store_group_t group, const char *name, 
+                             struct apr_array_header_t **ppubcert, apr_pool_t *p)
+{
+    return md_store_load(store, group, name, MD_FN_PUBCERT, MD_SV_CHAIN, (void**)ppubcert, p);
+}
+
+apr_status_t md_pubcert_save(md_store_t *store, apr_pool_t *p, 
+                             md_store_group_t group, const char *name, 
+                             struct apr_array_header_t *pubcert, int create)
+{
+    return md_store_save(store, p, group, name, MD_FN_PUBCERT, MD_SV_CHAIN, pubcert, create);
+}
+
+typedef struct {
+    md_store_t *store;
+    md_store_group_t group;
+    const char *pattern;
+    const char *aspect;
+    md_store_md_inspect *inspect;
+    void *baton;
+} inspect_md_ctx;
+
+static int insp_md(void *baton, const char *name, const char *aspect, 
+                   md_store_vtype_t vtype, void *value, apr_pool_t *ptemp)
+{
+    inspect_md_ctx *ctx = baton;
+    
+    if (!strcmp(MD_FN_MD, aspect) && vtype == MD_SV_JSON) {
+        md_t *md = md_from_json(value, ptemp);
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, ptemp, "inspecting md at: %s", name);
+        return ctx->inspect(ctx->baton, ctx->store, md, ptemp);
+    }
+    return 1;
+}
+
+apr_status_t md_store_md_iter(md_store_md_inspect *inspect, void *baton, md_store_t *store, 
+                              apr_pool_t *p, md_store_group_t group, const char *pattern)
+{
+    inspect_md_ctx ctx;
+    
+    ctx.store = store;
+    ctx.group = group;
+    ctx.inspect = inspect;
+    ctx.baton = baton;
+    
+    return md_store_iter(insp_md, &ctx, store, p, group, pattern, MD_FN_MD, MD_SV_JSON);
+}
+
diff --git a/modules/md/md_store.h b/modules/md/md_store.h
new file mode 100644 (file)
index 0000000..8f968c7
--- /dev/null
@@ -0,0 +1,156 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef mod_md_md_store_h
+#define mod_md_md_store_h
+
+struct apr_array_header_t;
+struct md_cert_t;
+struct md_pkey_t;
+
+typedef struct md_store_t md_store_t;
+
+typedef void md_store_destroy_cb(md_store_t *store);
+
+const char *md_store_group_name(int group);
+
+
+typedef apr_status_t md_store_load_cb(md_store_t *store, md_store_group_t group, 
+                                      const char *name, const char *aspect, 
+                                      md_store_vtype_t vtype, void **pvalue, 
+                                      apr_pool_t *p);
+typedef apr_status_t md_store_save_cb(md_store_t *store, apr_pool_t *p, md_store_group_t group, 
+                                      const char *name, const char *aspect, 
+                                      md_store_vtype_t vtype, void *value, 
+                                      int create);
+typedef apr_status_t md_store_remove_cb(md_store_t *store, md_store_group_t group, 
+                                        const char *name, const char *aspect,  
+                                        apr_pool_t *p, int force);
+typedef apr_status_t md_store_purge_cb(md_store_t *store, apr_pool_t *p, md_store_group_t group, 
+                                        const char *name);
+
+typedef int md_store_inspect(void *baton, const char *name, const char *aspect, 
+                             md_store_vtype_t vtype, void *value, apr_pool_t *ptemp);
+
+typedef apr_status_t md_store_iter_cb(md_store_inspect *inspect, void *baton, md_store_t *store, 
+                                      apr_pool_t *p, md_store_group_t group, const char *pattern,
+                                      const char *aspect, md_store_vtype_t vtype);
+
+typedef apr_status_t md_store_move_cb(md_store_t *store, apr_pool_t *p, md_store_group_t from, 
+                                      md_store_group_t to, const char *name, int archive);
+
+typedef apr_status_t md_store_get_fname_cb(const char **pfname, 
+                                           md_store_t *store, md_store_group_t group, 
+                                           const char *name, const char *aspect, 
+                                           apr_pool_t *p);
+
+typedef int md_store_is_newer_cb(md_store_t *store, 
+                                 md_store_group_t group1, md_store_group_t group2,  
+                                 const char *name, const char *aspect, apr_pool_t *p);
+
+struct md_store_t {
+    md_store_destroy_cb *destroy;
+
+    md_store_save_cb *save;
+    md_store_load_cb *load;
+    md_store_remove_cb *remove;
+    md_store_move_cb *move;
+    md_store_iter_cb *iterate;
+    md_store_purge_cb *purge;
+    md_store_get_fname_cb *get_fname;
+    md_store_is_newer_cb *is_newer;
+};
+
+void md_store_destroy(md_store_t *store);
+
+apr_status_t md_store_load_json(md_store_t *store, md_store_group_t group, 
+                                const char *name, const char *aspect, 
+                                struct md_json_t **pdata, apr_pool_t *p);
+apr_status_t md_store_save_json(md_store_t *store, apr_pool_t *p, md_store_group_t group, 
+                                const char *name, const char *aspect, 
+                                struct md_json_t *data, int create);
+
+
+apr_status_t md_store_load(md_store_t *store, md_store_group_t group, 
+                           const char *name, const char *aspect, 
+                           md_store_vtype_t vtype, void **pdata, 
+                           apr_pool_t *p);
+apr_status_t md_store_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, 
+                           const char *name, const char *aspect, 
+                           md_store_vtype_t vtype, void *data, 
+                           int create);
+apr_status_t md_store_remove(md_store_t *store, md_store_group_t group, 
+                             const char *name, const char *aspect, 
+                             apr_pool_t *p, int force);
+apr_status_t md_store_purge(md_store_t *store, apr_pool_t *p, 
+                            md_store_group_t group, const char *name);
+
+
+apr_status_t md_store_iter(md_store_inspect *inspect, void *baton, md_store_t *store, 
+                           apr_pool_t *p, md_store_group_t group, const char *pattern, 
+                           const char *aspect, md_store_vtype_t vtype);
+
+apr_status_t md_store_move(md_store_t *store, apr_pool_t *p,
+                           md_store_group_t from, md_store_group_t to,
+                           const char *name, int archive);
+
+apr_status_t md_store_get_fname(const char **pfname, 
+                                md_store_t *store, md_store_group_t group, 
+                                const char *name, const char *aspect, 
+                                apr_pool_t *p);
+
+int md_store_is_newer(md_store_t *store, md_store_group_t group1, md_store_group_t group2,  
+                      const char *name, const char *aspect, apr_pool_t *p);
+
+/**************************************************************************************************/
+/* Storage handling utils */
+
+apr_status_t md_load(md_store_t *store, md_store_group_t group, 
+                     const char *name, md_t **pmd, apr_pool_t *p);
+apr_status_t md_save(struct md_store_t *store, apr_pool_t *p, md_store_group_t group, 
+                     md_t *md, int create);
+apr_status_t md_remove(md_store_t *store, apr_pool_t *p, md_store_group_t group, 
+                     const char *name, int force);
+
+int md_is_newer(md_store_t *store, md_store_group_t group1, md_store_group_t group2,  
+                const char *name, apr_pool_t *p);
+
+typedef int md_store_md_inspect(void *baton, md_store_t *store, md_t *md, apr_pool_t *ptemp);
+
+apr_status_t md_store_md_iter(md_store_md_inspect *inspect, void *baton, md_store_t *store, 
+                              apr_pool_t *p, md_store_group_t group, const char *pattern);
+
+
+apr_status_t md_pkey_load(md_store_t *store, md_store_group_t group, 
+                          const char *name, struct md_pkey_t **ppkey, apr_pool_t *p);
+apr_status_t md_pkey_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, 
+                          const char *name, struct md_pkey_t *pkey, int create);
+apr_status_t md_cert_load(md_store_t *store, md_store_group_t group, 
+                          const char *name, struct md_cert_t **pcert, apr_pool_t *p);
+apr_status_t md_cert_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, 
+                          const char *name, struct md_cert_t *cert, int create);
+apr_status_t md_chain_load(md_store_t *store, md_store_group_t group, 
+                           const char *name, struct apr_array_header_t **pchain, apr_pool_t *p);
+apr_status_t md_chain_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, 
+                           const char *name, struct apr_array_header_t *chain, int create);
+
+apr_status_t md_pubcert_load(md_store_t *store, md_store_group_t group, const char *name, 
+                             struct apr_array_header_t **ppubcert, apr_pool_t *p);
+apr_status_t md_pubcert_save(md_store_t *store, apr_pool_t *p, 
+                             md_store_group_t group, const char *name, 
+                             struct apr_array_header_t *pubcert, int create);
+
+
+#endif /* mod_md_md_store_h */
diff --git a/modules/md/md_store_fs.c b/modules/md/md_store_fs.c
new file mode 100644 (file)
index 0000000..1c310bd
--- /dev/null
@@ -0,0 +1,888 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <apr_lib.h>
+#include <apr_file_info.h>
+#include <apr_file_io.h>
+#include <apr_fnmatch.h>
+#include <apr_hash.h>
+#include <apr_strings.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_json.h"
+#include "md_log.h"
+#include "md_store.h"
+#include "md_store_fs.h"
+#include "md_util.h"
+#include "md_version.h"
+
+/**************************************************************************************************/
+/* file system based implementation of md_store_t */
+
+#define MD_STORE_VERSION        3
+
+typedef struct {
+    apr_fileperms_t dir;
+    apr_fileperms_t file;
+} perms_t;
+
+typedef struct md_store_fs_t md_store_fs_t;
+struct md_store_fs_t {
+    md_store_t s;
+    
+    const char *base;       /* base directory of store */
+    perms_t def_perms;
+    perms_t group_perms[MD_SG_COUNT];
+    md_store_fs_cb *event_cb;
+    void *event_baton;
+    
+    const unsigned char *key;
+    apr_size_t key_len;
+    int plain_pkey[MD_SG_COUNT];
+    
+    int port_80;
+    int port_443;
+};
+
+#define FS_STORE(store)     (md_store_fs_t*)(((char*)store)-offsetof(md_store_fs_t, s))
+#define FS_STORE_JSON       "md_store.json"
+#define FS_STORE_KLEN       48
+
+static apr_status_t fs_load(md_store_t *store, md_store_group_t group, 
+                            const char *name, const char *aspect,  
+                            md_store_vtype_t vtype, void **pvalue, apr_pool_t *p);
+static apr_status_t fs_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, 
+                            const char *name, const char *aspect,  
+                            md_store_vtype_t vtype, void *value, int create);
+static apr_status_t fs_remove(md_store_t *store, md_store_group_t group, 
+                              const char *name, const char *aspect, 
+                              apr_pool_t *p, int force);
+static apr_status_t fs_purge(md_store_t *store, apr_pool_t *p, 
+                             md_store_group_t group, const char *name);
+static apr_status_t fs_move(md_store_t *store, apr_pool_t *p, 
+                            md_store_group_t from, md_store_group_t to, 
+                            const char *name, int archive);
+static apr_status_t fs_iterate(md_store_inspect *inspect, void *baton, md_store_t *store, 
+                               apr_pool_t *p, md_store_group_t group,  const char *pattern,
+                               const char *aspect, md_store_vtype_t vtype);
+
+static apr_status_t fs_get_fname(const char **pfname, 
+                                 md_store_t *store, md_store_group_t group, 
+                                 const char *name, const char *aspect, 
+                                 apr_pool_t *p);
+static int fs_is_newer(md_store_t *store, md_store_group_t group1, md_store_group_t group2,  
+                       const char *name, const char *aspect, apr_pool_t *p);
+
+static apr_status_t init_store_file(md_store_fs_t *s_fs, const char *fname, 
+                                    apr_pool_t *p, apr_pool_t *ptemp)
+{
+    md_json_t *json = md_json_create(p);
+    const char *key64;
+    unsigned char *key;
+    apr_status_t rv;
+    
+    md_json_setn(MD_STORE_VERSION, json, MD_KEY_STORE, MD_KEY_VERSION, NULL);
+
+    s_fs->key_len = FS_STORE_KLEN;
+    s_fs->key = key = apr_pcalloc(p, FS_STORE_KLEN);
+    if (APR_SUCCESS != (rv = md_rand_bytes(key, s_fs->key_len, p))) {
+        return rv;
+    }
+        
+    key64 = md_util_base64url_encode((char *)key, s_fs->key_len, ptemp);
+    md_json_sets(key64, json, MD_KEY_KEY, NULL);
+    rv = md_json_fcreatex(json, ptemp, MD_JSON_FMT_INDENT, fname, MD_FPROT_F_UONLY);
+    memset((char*)key64, 0, strlen(key64));
+
+    return rv;
+}
+
+static apr_status_t rename_pkey(void *baton, apr_pool_t *p, apr_pool_t *ptemp, 
+                                const char *dir, const char *name, 
+                                apr_filetype_e ftype)
+{
+    const char *from, *to;
+    apr_status_t rv = APR_SUCCESS;
+
+    (void)baton;
+    (void)ftype;
+    if (APR_SUCCESS == (rv = md_util_path_merge(&from, ptemp, dir, name, NULL))
+        && APR_SUCCESS == (rv = md_util_path_merge(&to, ptemp, dir, MD_FN_PRIVKEY, NULL))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "renaming %s/%s to %s", 
+                      dir, name, MD_FN_PRIVKEY);
+        return apr_file_rename(from, to, ptemp);
+    }
+    return rv;
+}
+
+static apr_status_t mk_pubcert(void *baton, apr_pool_t *p, apr_pool_t *ptemp, 
+                               const char *dir, const char *name, 
+                               apr_filetype_e ftype)
+{
+    md_cert_t *cert;
+    apr_array_header_t *chain, *pubcert;
+    const char *fname, *fpubcert;
+    apr_status_t rv = APR_SUCCESS;
+    
+    (void)baton;
+    (void)ftype;
+    (void)p;
+    if (   APR_SUCCESS == (rv = md_util_path_merge(&fpubcert, ptemp, dir, MD_FN_PUBCERT, NULL))
+        && APR_STATUS_IS_ENOENT((rv = md_chain_fload(&pubcert, ptemp, fpubcert)))
+        && APR_SUCCESS == (rv = md_util_path_merge(&fname, ptemp, dir, name, NULL))
+        && APR_SUCCESS == (rv = md_cert_fload(&cert, ptemp, fname))
+        && APR_SUCCESS == (rv = md_util_path_merge(&fname, ptemp, dir, MD_FN_CHAIN, NULL))) {
+        
+        rv = md_chain_fload(&chain, ptemp, fname);
+        if (APR_STATUS_IS_ENOENT(rv)) {
+            chain = apr_array_make(ptemp, 1, sizeof(md_cert_t*));
+            rv = APR_SUCCESS;
+        }
+        if (APR_SUCCESS == rv) {
+            pubcert = apr_array_make(ptemp, chain->nelts + 1, sizeof(md_cert_t*));
+            APR_ARRAY_PUSH(pubcert, md_cert_t *) = cert;
+            apr_array_cat(pubcert, chain);
+            rv = md_chain_fsave(pubcert, ptemp, fpubcert, MD_FPROT_F_UONLY);
+        }
+    }
+    return rv;
+}
+
+static apr_status_t upgrade_from_1_0(md_store_fs_t *s_fs, apr_pool_t *p, apr_pool_t *ptemp)
+{
+    md_store_group_t g;
+    apr_status_t rv = APR_SUCCESS;
+    
+    (void)ptemp;
+    /* Migrate pkey.pem -> privkey.pem */
+    for (g = MD_SG_NONE; g < MD_SG_COUNT && APR_SUCCESS == rv; ++g) {
+        rv = md_util_files_do(rename_pkey, s_fs, p, s_fs->base, 
+                              md_store_group_name(g), "*", "pkey.pem", NULL);
+    }
+    /* Generate fullcert.pem from cert.pem and chain.pem where missing */
+    rv = md_util_files_do(mk_pubcert, s_fs, p, s_fs->base, 
+                          md_store_group_name(MD_SG_DOMAINS), "*", MD_FN_CERT, NULL);
+    rv = md_util_files_do(mk_pubcert, s_fs, p, s_fs->base, 
+                          md_store_group_name(MD_SG_ARCHIVE), "*", MD_FN_CERT, NULL);
+    
+    return rv;
+}
+
+static apr_status_t read_store_file(md_store_fs_t *s_fs, const char *fname, 
+                                    apr_pool_t *p, apr_pool_t *ptemp)
+{
+    md_json_t *json;
+    const char *key64, *key;
+    apr_status_t rv;
+    double store_version;
+    
+    if (APR_SUCCESS == (rv = md_json_readf(&json, p, fname))) {
+        store_version = md_json_getn(json, MD_KEY_STORE, MD_KEY_VERSION, NULL);
+        if (store_version <= 0.0) {
+            /* ok, an old one, compatible to 1.0 */
+            store_version = 1.0;
+        }
+        if (store_version > MD_STORE_VERSION) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "version too new: %s", store_version);
+            return APR_EINVAL;
+        }
+
+        key64 = md_json_dups(p, json, MD_KEY_KEY, NULL);
+        if (!key64) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "missing key: %s", MD_KEY_KEY);
+            return APR_EINVAL;
+        }
+        
+        s_fs->key_len = md_util_base64url_decode(&key, key64, p);
+        s_fs->key = (const unsigned char*)key;
+        if (s_fs->key_len != FS_STORE_KLEN) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, 0, p, "key length unexpected: %d", 
+                          s_fs->key_len);
+            return APR_EINVAL;
+        }
+
+        /* Need to migrate format? */
+        if (store_version < MD_STORE_VERSION) {
+            if (store_version <= 1.0) {
+                md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "migrating store v1 -> v2");
+                rv = upgrade_from_1_0(s_fs, p, ptemp);
+            }
+            if (store_version <= 2.0) {
+                md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, p, "migrating store v2 -> v3");
+                md_json_del(json, MD_KEY_VERSION, NULL);
+            }
+            
+            if (APR_SUCCESS == rv) {
+                md_json_setn(MD_STORE_VERSION, json, MD_KEY_STORE, MD_KEY_VERSION, NULL);
+                rv = md_json_freplace(json, ptemp, MD_JSON_FMT_INDENT, fname, MD_FPROT_F_UONLY);
+           }
+           md_log_perror(MD_LOG_MARK, MD_LOG_INFO, rv, p, "migrated store");
+        } 
+    }
+    return rv;
+}
+
+static apr_status_t setup_store_file(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    md_store_fs_t *s_fs = baton;
+    const char *fname;
+    apr_status_t rv;
+
+    (void)ap;
+    s_fs->plain_pkey[MD_SG_DOMAINS] = 1;
+    s_fs->plain_pkey[MD_SG_TMP] = 1;
+    
+    rv = md_util_path_merge(&fname, ptemp, s_fs->base, FS_STORE_JSON, NULL);
+    if (APR_SUCCESS != rv) {
+        return rv;
+    }
+    
+read:
+    if (APR_SUCCESS == (rv = md_util_is_file(fname, ptemp))) {
+        rv = read_store_file(s_fs, fname, p, ptemp);
+    }
+    else if (APR_STATUS_IS_ENOENT(rv)) {
+        rv = init_store_file(s_fs, fname, p, ptemp);
+        if (APR_STATUS_IS_EEXIST(rv)) {
+            goto read;
+        }
+    }
+    return rv;
+}
+
+apr_status_t md_store_fs_init(md_store_t **pstore, apr_pool_t *p, const char *path)
+{
+    md_store_fs_t *s_fs;
+    apr_status_t rv = APR_SUCCESS;
+    
+    s_fs = apr_pcalloc(p, sizeof(*s_fs));
+
+    s_fs->s.load = fs_load;
+    s_fs->s.save = fs_save;
+    s_fs->s.remove = fs_remove;
+    s_fs->s.move = fs_move;
+    s_fs->s.purge = fs_purge;
+    s_fs->s.iterate = fs_iterate;
+    s_fs->s.get_fname = fs_get_fname;
+    s_fs->s.is_newer = fs_is_newer;
+    
+    /* by default, everything is only readable by the current user */ 
+    s_fs->def_perms.dir = MD_FPROT_D_UONLY;
+    s_fs->def_perms.file = MD_FPROT_F_UONLY;
+
+    /* Account information needs to be accessible to httpd child processes.
+     * private keys are, similar to staging, encrypted. */
+    s_fs->group_perms[MD_SG_ACCOUNTS].dir = MD_FPROT_D_UALL_WREAD;
+    s_fs->group_perms[MD_SG_ACCOUNTS].file = MD_FPROT_F_UALL_WREAD;
+    s_fs->group_perms[MD_SG_STAGING].dir = MD_FPROT_D_UALL_WREAD;
+    s_fs->group_perms[MD_SG_STAGING].file = MD_FPROT_F_UALL_WREAD;
+    /* challenges dir and files are readable by all, no secrets involved */ 
+    s_fs->group_perms[MD_SG_CHALLENGES].dir = MD_FPROT_D_UALL_WREAD;
+    s_fs->group_perms[MD_SG_CHALLENGES].file = MD_FPROT_F_UALL_WREAD;
+
+    s_fs->base = apr_pstrdup(p, path);
+    
+    if (APR_SUCCESS != (rv = md_util_is_dir(s_fs->base, p))) {
+        if (APR_STATUS_IS_ENOENT(rv)) {
+            rv = apr_dir_make_recursive(s_fs->base, s_fs->def_perms.dir, p);
+            if (APR_SUCCESS == rv) {
+                rv = apr_file_perms_set(s_fs->base, MD_FPROT_D_UALL_WREAD);
+                if (APR_STATUS_IS_ENOTIMPL(rv)) {
+                    rv = APR_SUCCESS;
+                }
+            }
+        }
+    }
+    rv = md_util_pool_vdo(setup_store_file, s_fs, p, NULL);
+    
+    if (APR_SUCCESS != rv) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, p, "init fs store at %s", path);
+    }
+    *pstore = (rv == APR_SUCCESS)? &(s_fs->s) : NULL;
+    return rv;
+}
+
+apr_status_t md_store_fs_default_perms_set(md_store_t *store, 
+                                           apr_fileperms_t file_perms,
+                                           apr_fileperms_t dir_perms)
+{
+    md_store_fs_t *s_fs = FS_STORE(store);
+    
+    s_fs->def_perms.file = file_perms;
+    s_fs->def_perms.dir = dir_perms;
+    return APR_SUCCESS;
+}
+
+apr_status_t md_store_fs_group_perms_set(md_store_t *store, md_store_group_t group, 
+                                         apr_fileperms_t file_perms,
+                                         apr_fileperms_t dir_perms)
+{
+    md_store_fs_t *s_fs = FS_STORE(store);
+    
+    if (group >= (sizeof(s_fs->group_perms)/sizeof(s_fs->group_perms[0]))) {
+        return APR_ENOTIMPL;
+    }
+    s_fs->group_perms[group].file = file_perms;
+    s_fs->group_perms[group].dir = dir_perms;
+    return APR_SUCCESS;
+}
+
+apr_status_t md_store_fs_set_event_cb(struct md_store_t *store, md_store_fs_cb *cb, void *baton)
+{
+    md_store_fs_t *s_fs = FS_STORE(store);
+    
+    s_fs->event_cb = cb;
+    s_fs->event_baton = baton;
+    return APR_SUCCESS;
+}
+
+static const perms_t *gperms(md_store_fs_t *s_fs, md_store_group_t group)
+{
+    if (group >= (sizeof(s_fs->group_perms)/sizeof(s_fs->group_perms[0]))
+        || !s_fs->group_perms[group].dir) {
+        return &s_fs->def_perms;
+    }
+    return &s_fs->group_perms[group];
+}
+
+static apr_status_t fs_get_fname(const char **pfname, 
+                                 md_store_t *store, md_store_group_t group, 
+                                 const char *name, const char *aspect, 
+                                 apr_pool_t *p)
+{
+    md_store_fs_t *s_fs = FS_STORE(store);
+    if (group == MD_SG_NONE) {
+        return md_util_path_merge(pfname, p, s_fs->base, aspect, NULL);
+    }
+    return md_util_path_merge(pfname, p, 
+                              s_fs->base, md_store_group_name(group), name, aspect, NULL);
+}
+
+static apr_status_t fs_get_dname(const char **pdname, 
+                                 md_store_t *store, md_store_group_t group, 
+                                 const char *name, apr_pool_t *p)
+{
+    md_store_fs_t *s_fs = FS_STORE(store);
+    if (group == MD_SG_NONE) {
+        *pdname = s_fs->base;
+        return APR_SUCCESS;
+    }
+    return md_util_path_merge(pdname, p, s_fs->base, md_store_group_name(group), name, NULL);
+}
+
+static void get_pass(const char **ppass, apr_size_t *plen, 
+                     md_store_fs_t *s_fs, md_store_group_t group)
+{
+    if (s_fs->plain_pkey[group]) {
+        *ppass = NULL;
+        *plen = 0;
+    }
+    else {
+        *ppass = (const char *)s_fs->key;
+        *plen = s_fs->key_len;
+    }
+}
+static apr_status_t fs_fload(void **pvalue, md_store_fs_t *s_fs, const char *fpath, 
+                             md_store_group_t group, md_store_vtype_t vtype, 
+                             apr_pool_t *p, apr_pool_t *ptemp)
+{
+    apr_status_t rv;
+    const char *pass;
+    apr_size_t pass_len;
+    
+    if (pvalue != NULL) {
+        switch (vtype) {
+            case MD_SV_TEXT:
+                rv = md_text_fread8k((const char **)pvalue, p, fpath);
+                break;
+            case MD_SV_JSON:
+                rv = md_json_readf((md_json_t **)pvalue, p, fpath);
+                break;
+            case MD_SV_CERT:
+                rv = md_cert_fload((md_cert_t **)pvalue, p, fpath);
+                break;
+            case MD_SV_PKEY:
+                get_pass(&pass, &pass_len, s_fs, group);
+                rv = md_pkey_fload((md_pkey_t **)pvalue, p, pass, pass_len, fpath);
+                break;
+            case MD_SV_CHAIN:
+                rv = md_chain_fload((apr_array_header_t **)pvalue, p, fpath);
+                break;
+            default:
+                rv = APR_ENOTIMPL;
+                break;
+        }
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE2, rv, ptemp, 
+                      "loading type %d from %s", vtype, fpath);
+    }
+    else { /* check for existence only */
+        rv = md_util_is_file(fpath, p);
+    }
+    return rv;
+}
+
+static apr_status_t pfs_load(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    md_store_fs_t *s_fs = baton;
+    const char *fpath, *name, *aspect;
+    md_store_vtype_t vtype;
+    md_store_group_t group;
+    void **pvalue;
+    apr_status_t rv;
+    
+    group = (md_store_group_t)va_arg(ap, int);
+    name = va_arg(ap, const char *);
+    aspect = va_arg(ap, const char *);
+    vtype = (md_store_vtype_t)va_arg(ap, int);
+    pvalue= va_arg(ap, void **);
+        
+    rv = fs_get_fname(&fpath, &s_fs->s, group, name, aspect, ptemp);
+    if (APR_SUCCESS == rv) {
+        rv = fs_fload(pvalue, s_fs, fpath, group, vtype, p, ptemp);
+    }
+    return rv;
+}
+
+static apr_status_t dispatch(md_store_fs_t *s_fs, md_store_fs_ev_t ev, int group, 
+                             const char *fname, apr_filetype_e ftype, apr_pool_t *p)
+{
+    (void)ev;
+    if (s_fs->event_cb) {
+        return s_fs->event_cb(s_fs->event_baton, &s_fs->s, MD_S_FS_EV_CREATED, 
+                              group, fname, ftype, p);
+    }
+    return APR_SUCCESS;
+}
+
+static apr_status_t mk_group_dir(const char **pdir, md_store_fs_t *s_fs, 
+                                 md_store_group_t group, const char *name,
+                                 apr_pool_t *p)
+{
+    const perms_t *perms;
+    apr_status_t rv;
+    
+    perms = gperms(s_fs, group);
+
+    if (APR_SUCCESS == (rv = fs_get_dname(pdir, &s_fs->s, group, name, p))
+        && (MD_SG_NONE != group)) {
+        if (APR_SUCCESS != md_util_is_dir(*pdir, p)) {
+            if (APR_SUCCESS == (rv = apr_dir_make_recursive(*pdir, perms->dir, p))) {
+                rv = dispatch(s_fs, MD_S_FS_EV_CREATED, group, *pdir, APR_DIR, p);
+            }
+        }
+        else {
+            /* already exists */
+        }
+        
+        if (APR_SUCCESS == rv) {
+            rv = apr_file_perms_set(*pdir, perms->dir);
+            md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "mk_group_dir %s perm set", *pdir);
+            if (APR_STATUS_IS_ENOTIMPL(rv)) {
+                rv = APR_SUCCESS;
+            }
+        }
+    }
+    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "mk_group_dir %d %s", group, name);
+    return rv;
+}
+
+static apr_status_t pfs_is_newer(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    md_store_fs_t *s_fs = baton;
+    const char *fname1, *fname2, *name, *aspect;
+    md_store_group_t group1, group2;
+    apr_finfo_t inf1, inf2;
+    int *pnewer;
+    apr_status_t rv;
+    
+    (void)p;
+    group1 = (md_store_group_t)va_arg(ap, int);
+    group2 = (md_store_group_t)va_arg(ap, int);
+    name = va_arg(ap, const char*);
+    aspect = va_arg(ap, const char*);
+    pnewer = va_arg(ap, int*);
+    
+    *pnewer = 0;
+    if (   APR_SUCCESS == (rv = fs_get_fname(&fname1, &s_fs->s, group1, name, aspect, ptemp))
+        && APR_SUCCESS == (rv = fs_get_fname(&fname2, &s_fs->s, group2, name, aspect, ptemp))
+        && APR_SUCCESS == (rv = apr_stat(&inf1, fname1, APR_FINFO_MTIME, ptemp))
+        && APR_SUCCESS == (rv = apr_stat(&inf2, fname2, APR_FINFO_MTIME, ptemp))) {
+        *pnewer = inf1.mtime > inf2.mtime;
+    }
+
+    return rv;
+}
+
+static int fs_is_newer(md_store_t *store, md_store_group_t group1, md_store_group_t group2,  
+                       const char *name, const char *aspect, apr_pool_t *p)
+{
+    md_store_fs_t *s_fs = FS_STORE(store);
+    int newer = 0;
+    apr_status_t rv;
+    
+    rv = md_util_pool_vdo(pfs_is_newer, s_fs, p, group1, group2, name, aspect, &newer, NULL);
+    if (APR_SUCCESS == rv) {
+        return newer;
+    }
+    return 0;
+}
+
+static apr_status_t pfs_save(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    md_store_fs_t *s_fs = baton;
+    const char *gdir, *dir, *fpath, *name, *aspect;
+    md_store_vtype_t vtype;
+    md_store_group_t group;
+    void *value;
+    int create;
+    apr_status_t rv;
+    const perms_t *perms;
+    const char *pass;
+    apr_size_t pass_len;
+    
+    group = (md_store_group_t)va_arg(ap, int);
+    name = va_arg(ap, const char*);
+    aspect = va_arg(ap, const char*);
+    vtype = (md_store_vtype_t)va_arg(ap, int);
+    value = va_arg(ap, void *);
+    create = va_arg(ap, int);
+    
+    perms = gperms(s_fs, group);
+    
+    if (APR_SUCCESS == (rv = mk_group_dir(&gdir, s_fs, group, NULL, p)) 
+        && APR_SUCCESS == (rv = mk_group_dir(&dir, s_fs, group, name, p))
+        && APR_SUCCESS == (rv = md_util_path_merge(&fpath, ptemp, dir, aspect, NULL))) {
+        
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, ptemp, "storing in %s", fpath);
+        switch (vtype) {
+            case MD_SV_TEXT:
+                rv = (create? md_text_fcreatex(fpath, perms->file, p, value)
+                      : md_text_freplace(fpath, perms->file, p, value));
+                break;
+            case MD_SV_JSON:
+                rv = (create? md_json_fcreatex((md_json_t *)value, p, MD_JSON_FMT_INDENT, 
+                                               fpath, perms->file)
+                      : md_json_freplace((md_json_t *)value, p, MD_JSON_FMT_INDENT, 
+                                         fpath, perms->file));
+                break;
+            case MD_SV_CERT:
+                rv = md_cert_fsave((md_cert_t *)value, ptemp, fpath, perms->file);
+                break;
+            case MD_SV_PKEY:
+                /* Take care that we write private key with access only to the user,
+                 * unless we write the key encrypted */
+                get_pass(&pass, &pass_len, s_fs, group);
+                rv = md_pkey_fsave((md_pkey_t *)value, ptemp, pass, pass_len, 
+                                   fpath, (pass && pass_len)? perms->file : MD_FPROT_F_UONLY);
+                break;
+            case MD_SV_CHAIN:
+                rv = md_chain_fsave((apr_array_header_t*)value, ptemp, fpath, perms->file);
+                break;
+            default:
+                return APR_ENOTIMPL;
+        }
+        if (APR_SUCCESS == rv) {
+            rv = dispatch(s_fs, MD_S_FS_EV_CREATED, group, fpath, APR_REG, p);
+        }
+    }
+    return rv;
+}
+
+static apr_status_t pfs_remove(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    md_store_fs_t *s_fs = baton;
+    const char *dir, *name, *fpath, *groupname, *aspect;
+    apr_status_t rv;
+    int force;
+    apr_finfo_t info;
+    md_store_group_t group;
+    
+    (void)p;
+    group = (md_store_group_t)va_arg(ap, int);
+    name = va_arg(ap, const char*);
+    aspect = va_arg(ap, const char *);
+    force = va_arg(ap, int);
+    
+    groupname = md_store_group_name(group);
+    
+    if (APR_SUCCESS == (rv = md_util_path_merge(&dir, ptemp, s_fs->base, groupname, name, NULL))
+        && APR_SUCCESS == (rv = md_util_path_merge(&fpath, ptemp, dir, aspect, NULL))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, 0, ptemp, "start remove of md %s/%s/%s", 
+                      groupname, name, aspect);
+
+        if (APR_SUCCESS != (rv = apr_stat(&info, dir, APR_FINFO_TYPE, ptemp))) {
+            if (APR_ENOENT == rv && force) {
+                return APR_SUCCESS;
+            }
+            return rv;
+        }
+    
+        rv = apr_file_remove(fpath, ptemp);
+        if (APR_ENOENT == rv && force) {
+            rv = APR_SUCCESS;
+        }
+    }
+    return rv;
+}
+
+static apr_status_t fs_load(md_store_t *store, md_store_group_t group, 
+                            const char *name, const char *aspect,  
+                            md_store_vtype_t vtype, void **pvalue, apr_pool_t *p)
+{
+    md_store_fs_t *s_fs = FS_STORE(store);
+    return md_util_pool_vdo(pfs_load, s_fs, p, group, name, aspect, vtype, pvalue, NULL);
+}
+
+static apr_status_t fs_save(md_store_t *store, apr_pool_t *p, md_store_group_t group, 
+                            const char *name, const char *aspect,  
+                            md_store_vtype_t vtype, void *value, int create)
+{
+    md_store_fs_t *s_fs = FS_STORE(store);
+    return md_util_pool_vdo(pfs_save, s_fs, p, group, name, aspect, 
+                            vtype, value, create, NULL);
+}
+
+static apr_status_t fs_remove(md_store_t *store, md_store_group_t group, 
+                              const char *name, const char *aspect, 
+                              apr_pool_t *p, int force)
+{
+    md_store_fs_t *s_fs = FS_STORE(store);
+    return md_util_pool_vdo(pfs_remove, s_fs, p, group, name, aspect, force, NULL);
+}
+
+static apr_status_t pfs_purge(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    md_store_fs_t *s_fs = baton;
+    const char *dir, *name, *groupname;
+    md_store_group_t group;
+    apr_status_t rv;
+    
+    (void)p;
+    group = (md_store_group_t)va_arg(ap, int);
+    name = va_arg(ap, const char*);
+    
+    groupname = md_store_group_name(group);
+
+    if (APR_SUCCESS == (rv = md_util_path_merge(&dir, ptemp, s_fs->base, groupname, name, NULL))) {
+        /* Remove all files in dir, there should be no sub-dirs */
+        rv = md_util_rm_recursive(dir, ptemp, 1);
+    }
+    md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "purge %s/%s (%s)", groupname, name, dir);
+    return APR_SUCCESS;
+}
+
+static apr_status_t fs_purge(md_store_t *store, apr_pool_t *p, 
+                             md_store_group_t group, const char *name)
+{
+    md_store_fs_t *s_fs = FS_STORE(store);
+    return md_util_pool_vdo(pfs_purge, s_fs, p, group, name, NULL);
+}
+
+/**************************************************************************************************/
+/* iteration */
+
+typedef struct {
+    md_store_fs_t *s_fs;
+    md_store_group_t group;
+    const char *pattern;
+    const char *aspect;
+    md_store_vtype_t vtype;
+    md_store_inspect *inspect;
+    void *baton;
+} inspect_ctx;
+
+static apr_status_t insp(void *baton, apr_pool_t *p, apr_pool_t *ptemp, 
+                         const char *dir, const char *name, apr_filetype_e ftype)
+{
+    inspect_ctx *ctx = baton;
+    apr_status_t rv;
+    void *value;
+    const char *fpath;
+    (void)ftype;   
+    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, ptemp, "inspecting value at: %s/%s", dir, name);
+    if (APR_SUCCESS == (rv = md_util_path_merge(&fpath, ptemp, dir, name, NULL))) {
+        rv = fs_fload(&value, ctx->s_fs, fpath, ctx->group, ctx->vtype, p, ptemp);
+        if (APR_SUCCESS == rv 
+            && !ctx->inspect(ctx->baton, name, ctx->aspect, ctx->vtype, value, ptemp)) {
+            return APR_EOF;
+        }
+    }
+    return rv;
+}
+
+static apr_status_t fs_iterate(md_store_inspect *inspect, void *baton, md_store_t *store, 
+                               apr_pool_t *p, md_store_group_t group, const char *pattern, 
+                               const char *aspect, md_store_vtype_t vtype)
+{
+    const char *groupname;
+    apr_status_t rv;
+    inspect_ctx ctx;
+    
+    ctx.s_fs = FS_STORE(store);
+    ctx.group = group;
+    ctx.pattern = pattern;
+    ctx.aspect = aspect;
+    ctx.vtype = vtype;
+    ctx.inspect = inspect;
+    ctx.baton = baton;
+    groupname = md_store_group_name(group);
+
+    rv = md_util_files_do(insp, &ctx, p, ctx.s_fs->base, groupname, ctx.pattern, aspect, NULL);
+    
+    return rv;
+}
+
+/**************************************************************************************************/
+/* moving */
+
+static apr_status_t pfs_move(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    md_store_fs_t *s_fs = baton;
+    const char *name, *from_group, *to_group, *from_dir, *to_dir, *arch_dir, *dir;
+    md_store_group_t from, to;
+    int archive;
+    apr_status_t rv;
+    
+    (void)p;
+    from = (md_store_group_t)va_arg(ap, int);
+    to = (md_store_group_t)va_arg(ap, int);
+    name = va_arg(ap, const char*);
+    archive = va_arg(ap, int);
+    
+    from_group = md_store_group_name(from);
+    to_group = md_store_group_name(to);
+    if (!strcmp(from_group, to_group)) {
+        return APR_EINVAL;
+    }
+
+    rv = md_util_path_merge(&from_dir, ptemp, s_fs->base, from_group, name, NULL);
+    if (APR_SUCCESS != rv) goto out;
+    rv = md_util_path_merge(&to_dir, ptemp, s_fs->base, to_group, name, NULL);
+    if (APR_SUCCESS != rv) goto out;
+    
+    if (APR_SUCCESS != (rv = md_util_is_dir(from_dir, ptemp))) {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "source is no dir: %s", from_dir);
+        goto out;
+    }
+    
+    rv = archive? md_util_is_dir(to_dir, ptemp) : APR_ENOENT;
+    if (APR_SUCCESS == rv) {
+        int n = 1;
+        const char *narch_dir;
+
+        rv = md_util_path_merge(&dir, ptemp, s_fs->base, md_store_group_name(MD_SG_ARCHIVE), NULL);
+        if (APR_SUCCESS != rv) goto out;
+        rv = apr_dir_make_recursive(dir, MD_FPROT_D_UONLY, ptemp); 
+        if (APR_SUCCESS != rv) goto out;
+        rv = md_util_path_merge(&arch_dir, ptemp, dir, name, NULL);
+        if (APR_SUCCESS != rv) goto out;
+        
+#ifdef WIN32
+        /* WIN32 and handling of files/dirs. What can one say? */
+        
+        while (n < 1000) {
+            narch_dir = apr_psprintf(ptemp, "%s.%d", arch_dir, n);
+            rv = md_util_is_dir(narch_dir, ptemp);
+            if (APR_STATUS_IS_ENOENT(rv)) {
+                md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "using archive dir: %s", 
+                              narch_dir);
+                break;
+            }
+            else {
+                ++n;
+                narch_dir = NULL;
+            }
+        }
+
+#else   /* ifdef WIN32 */
+
+        while (n < 1000) {
+            narch_dir = apr_psprintf(ptemp, "%s.%d", arch_dir, n);
+            rv = apr_dir_make(narch_dir, MD_FPROT_D_UONLY, ptemp);
+            if (APR_SUCCESS == rv) {
+                md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "using archive dir: %s", 
+                              narch_dir);
+                break;
+            }
+            else if (APR_EEXIST == rv) {
+                ++n;
+                narch_dir = NULL;
+            }
+            else {
+                md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "creating archive dir: %s", 
+                              narch_dir);
+                goto out;
+            }
+        }
+         
+#endif   /* ifdef WIN32 (else part) */
+        
+        if (!narch_dir) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "ran out of numbers less than 1000 "
+                          "while looking for an available one in %s to archive the data "
+                          "from %s. Either something is generally wrong or you need to "
+                          "clean up some of those directories.", arch_dir, from_dir);
+            rv = APR_EGENERAL;
+            goto out;
+        }
+        
+        if (APR_SUCCESS != (rv = apr_file_rename(to_dir, narch_dir, ptemp))) {
+                md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "rename from %s to %s", 
+                              to_dir, narch_dir);
+                goto out;
+        }
+        if (APR_SUCCESS != (rv = apr_file_rename(from_dir, to_dir, ptemp))) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "moving %s to %s: %s", 
+                          from_dir, to_dir);
+            apr_file_rename(narch_dir, to_dir, ptemp);
+            goto out;
+        }
+        rv = dispatch(s_fs, MD_S_FS_EV_MOVED, to, to_dir, APR_DIR, ptemp);
+        if (APR_SUCCESS == rv) {
+            rv = dispatch(s_fs, MD_S_FS_EV_MOVED, MD_SG_ARCHIVE, narch_dir, APR_DIR, ptemp);
+        }
+    }
+    else if (APR_STATUS_IS_ENOENT(rv)) {
+        if (APR_SUCCESS != (rv = apr_file_rename(from_dir, to_dir, ptemp))) {
+            md_log_perror(MD_LOG_MARK, MD_LOG_ERR, rv, ptemp, "rename from %s to %s", 
+                          from_dir, to_dir);
+            goto out;
+        }
+    }
+    else {
+        md_log_perror(MD_LOG_MARK, MD_LOG_DEBUG, rv, ptemp, "target is no dir: %s", to_dir);
+        goto out;
+    }
+    
+out:
+    return rv;
+}
+
+static apr_status_t fs_move(md_store_t *store, apr_pool_t *p, 
+                            md_store_group_t from, md_store_group_t to, 
+                            const char *name, int archive)
+{
+    md_store_fs_t *s_fs = FS_STORE(store);
+    return md_util_pool_vdo(pfs_move, s_fs, p, from, to, name, archive, NULL);
+}
diff --git a/modules/md/md_store_fs.h b/modules/md/md_store_fs.h
new file mode 100644 (file)
index 0000000..d2344be
--- /dev/null
@@ -0,0 +1,64 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef mod_md_md_store_fs_h
+#define mod_md_md_store_fs_h
+
+struct md_store_t;
+
+/** 
+ * Default file permissions set by the store, user only read/write(/exec),
+ * if so supported by the apr. 
+ */
+#define MD_FPROT_F_UONLY      (APR_FPROT_UREAD|APR_FPROT_UWRITE)
+#define MD_FPROT_D_UONLY      (MD_FPROT_F_UONLY|APR_FPROT_UEXECUTE)
+
+/**
+ * User has all permission, group can read, other none
+ */
+#define MD_FPROT_F_UALL_GREAD (MD_FPROT_F_UONLY|APR_FPROT_GREAD)
+#define MD_FPROT_D_UALL_GREAD (MD_FPROT_D_UONLY|APR_FPROT_GREAD|APR_FPROT_GEXECUTE)
+
+/**
+ * User has all permission, group and others can read
+ */
+#define MD_FPROT_F_UALL_WREAD (MD_FPROT_F_UALL_GREAD|APR_FPROT_WREAD)
+#define MD_FPROT_D_UALL_WREAD (MD_FPROT_D_UALL_GREAD|APR_FPROT_WREAD|APR_FPROT_WEXECUTE)
+
+apr_status_t md_store_fs_init(struct md_store_t **pstore, apr_pool_t *p, 
+                              const char *path);
+
+
+apr_status_t md_store_fs_default_perms_set(struct md_store_t *store, 
+                                           apr_fileperms_t file_perms,
+                                           apr_fileperms_t dir_perms);
+apr_status_t md_store_fs_group_perms_set(struct md_store_t *store, 
+                                         md_store_group_t group, 
+                                         apr_fileperms_t file_perms,
+                                         apr_fileperms_t dir_perms);
+
+typedef enum {
+    MD_S_FS_EV_CREATED,
+    MD_S_FS_EV_MOVED,
+} md_store_fs_ev_t; 
+
+typedef apr_status_t md_store_fs_cb(void *baton, struct md_store_t *store,
+                                    md_store_fs_ev_t ev, int group, 
+                                    const char *fname, apr_filetype_e ftype,  
+                                    apr_pool_t *p);
+                                    
+apr_status_t md_store_fs_set_event_cb(struct md_store_t *store, md_store_fs_cb *cb, void *baton);
+
+#endif /* mod_md_md_store_fs_h */
diff --git a/modules/md/md_util.c b/modules/md/md_util.c
new file mode 100644 (file)
index 0000000..bb6f609
--- /dev/null
@@ -0,0 +1,1253 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h>
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+#include <apr_portable.h>
+#include <apr_file_info.h>
+#include <apr_fnmatch.h>
+#include <apr_tables.h>
+#include <apr_uri.h>
+
+#include "md_log.h"
+#include "md_util.h"
+
+/**************************************************************************************************/
+/* pool utils */
+
+apr_status_t md_util_pool_do(md_util_action *cb, void *baton, apr_pool_t *p)
+{
+    apr_pool_t *ptemp;
+    apr_status_t rv = apr_pool_create(&ptemp, p);
+    if (APR_SUCCESS == rv) {
+        rv = cb(baton, p, ptemp);
+        
+        apr_pool_destroy(ptemp);
+    }
+    return rv;
+}
+static apr_status_t pool_vado(md_util_vaction *cb, void *baton, apr_pool_t *p, va_list ap)
+{
+    apr_pool_t *ptemp;
+    apr_status_t rv;
+    
+    rv = apr_pool_create(&ptemp, p);
+    if (APR_SUCCESS == rv) {
+        rv = cb(baton, p, ptemp, ap);
+        apr_pool_destroy(ptemp);
+    }
+    return rv;
+}
+apr_status_t md_util_pool_vdo(md_util_vaction *cb, void *baton, apr_pool_t *p, ...)
+{
+    va_list ap;
+    apr_status_t rv;
+    
+    va_start(ap, p);
+    rv = pool_vado(cb, baton, p, ap);
+    va_end(ap);
+    return rv;
+}
+/**************************************************************************************************/
+/* string related */
+
+char *md_util_str_tolower(char *s)
+{
+    char *orig = s;
+    while (*s) {
+        *s = (char)apr_tolower(*s);
+        ++s;
+    }
+    return orig;
+}
+
+int md_array_str_index(const apr_array_header_t *array, const char *s, 
+                       int start, int case_sensitive)
+{
+    if (start >= 0) {
+        int i;
+        
+        for (i = start; i < array->nelts; i++) {
+            const char *p = APR_ARRAY_IDX(array, i, const char *);
+            if ((case_sensitive && !strcmp(p, s))
+                || (!case_sensitive && !apr_strnatcasecmp(p, s))) {
+                return i;
+            }
+        }
+    }
+    
+    return -1;
+}
+
+int md_array_str_eq(const struct apr_array_header_t *a1, 
+                    const struct apr_array_header_t *a2, int case_sensitive)
+{
+    int i;
+    const char *s1, *s2;
+    
+    if (a1 == a2) return 1;
+    if (!a1) return 0;
+    if (a1->nelts != a2->nelts) return 0;
+    for (i = 0; i < a1->nelts; ++i) {
+        s1 = APR_ARRAY_IDX(a1, i, const char *);
+        s2 = APR_ARRAY_IDX(a2, i, const char *);
+        if ((case_sensitive && strcmp(s1, s2))
+            || (!case_sensitive && apr_strnatcasecmp(s1, s2))) {
+            return 0;
+        }
+    }
+    return 1;
+}
+
+apr_array_header_t *md_array_str_clone(apr_pool_t *p, apr_array_header_t *src)
+{
+    apr_array_header_t *dest = apr_array_make(p, src->nelts, sizeof(const char*));
+    if (dest) {
+        int i;
+        for (i = 0; i < src->nelts; i++) {
+            const char *s = APR_ARRAY_IDX(src, i, const char *);
+            APR_ARRAY_PUSH(dest, const char *) = apr_pstrdup(p, s); 
+        }
+    }
+    return dest;
+}
+
+struct apr_array_header_t *md_array_str_compact(apr_pool_t *p, struct apr_array_header_t *src,
+                                                int case_sensitive)
+{
+    apr_array_header_t *dest = apr_array_make(p, src->nelts, sizeof(const char*));
+    if (dest) {
+        const char *s;
+        int i;
+        for (i = 0; i < src->nelts; ++i) {
+            s = APR_ARRAY_IDX(src, i, const char *);
+            if (md_array_str_index(dest, s, 0, case_sensitive) < 0) {
+                APR_ARRAY_PUSH(dest, char *) = md_util_str_tolower(apr_pstrdup(p, s));
+            }
+        }
+    }
+    return dest;
+}
+
+apr_array_header_t *md_array_str_remove(apr_pool_t *p, apr_array_header_t *src, 
+                                        const char *exclude, int case_sensitive)
+{
+    apr_array_header_t *dest = apr_array_make(p, src->nelts, sizeof(const char*));
+    if (dest) {
+        int i;
+        for (i = 0; i < src->nelts; i++) {
+            const char *s = APR_ARRAY_IDX(src, i, const char *);
+            if (!exclude 
+                || (case_sensitive && strcmp(exclude, s))
+                || (!case_sensitive && apr_strnatcasecmp(exclude, s))) {
+                APR_ARRAY_PUSH(dest, const char *) = apr_pstrdup(p, s); 
+            }
+        }
+    }
+    return dest;
+}
+
+int md_array_str_add_missing(apr_array_header_t *dest, apr_array_header_t *src, int case_sensitive)
+{
+    int i, added = 0;
+    for (i = 0; i < src->nelts; i++) {
+        const char *s = APR_ARRAY_IDX(src, i, const char *);
+        if (md_array_str_index(dest, s, 0, case_sensitive) < 0) {
+            APR_ARRAY_PUSH(dest, const char *) = s;
+            ++added; 
+        }
+    }
+    return added;
+}
+
+/**************************************************************************************************/
+/* file system related */
+
+apr_status_t md_util_fopen(FILE **pf, const char *fn, const char *mode)
+{
+    *pf = fopen(fn, mode);
+    if (*pf == NULL) {
+        return errno;
+    }
+
+    return APR_SUCCESS;
+}
+
+apr_status_t md_util_fcreatex(apr_file_t **pf, const char *fn, 
+                              apr_fileperms_t perms, apr_pool_t *p)
+{
+    return apr_file_open(pf, fn, (APR_FOPEN_WRITE|APR_FOPEN_CREATE|APR_FOPEN_EXCL),
+                         perms, p);
+}
+
+apr_status_t md_util_is_dir(const char *path, apr_pool_t *pool)
+{
+    apr_finfo_t info;
+    apr_status_t rv = apr_stat(&info, path, APR_FINFO_TYPE, pool);
+    if (rv == APR_SUCCESS) {
+        rv = (info.filetype == APR_DIR)? APR_SUCCESS : APR_EINVAL;
+    }
+    return rv;
+}
+
+apr_status_t md_util_is_file(const char *path, apr_pool_t *pool)
+{
+    apr_finfo_t info;
+    apr_status_t rv = apr_stat(&info, path, APR_FINFO_TYPE, pool);
+    if (rv == APR_SUCCESS) {
+        rv = (info.filetype == APR_REG)? APR_SUCCESS : APR_EINVAL;
+    }
+    return rv;
+}
+
+apr_status_t md_util_path_merge(const char **ppath, apr_pool_t *p, ...)
+{
+    const char *segment, *path;
+    va_list ap;
+    apr_status_t rv = APR_SUCCESS;
+    
+    va_start(ap, p);
+    path = va_arg(ap, char *);
+    while (path && APR_SUCCESS == rv && (segment = va_arg(ap, char *))) {
+        rv = apr_filepath_merge((char **)&path, path, segment, APR_FILEPATH_SECUREROOT , p);
+    }
+    va_end(ap);
+    
+    *ppath = (APR_SUCCESS == rv)? (path? path : "") : NULL;
+    return rv;
+}
+
+apr_status_t md_util_freplace(const char *fpath, apr_fileperms_t perms, apr_pool_t *p, 
+                              md_util_file_cb *write_cb, void *baton)
+{
+    apr_status_t rv = APR_EEXIST;
+    apr_file_t *f;
+    const char *tmp;
+    int i, max;
+    
+    tmp = apr_psprintf(p, "%s.tmp", fpath);
+    i = 0; max = 20;
+creat:
+    while (i < max && APR_EEXIST == (rv = md_util_fcreatex(&f, tmp, perms, p))) {
+        ++i;
+        apr_sleep(apr_time_msec(50));
+    } 
+    if (APR_EEXIST == rv 
+        && APR_SUCCESS == (rv = apr_file_remove(tmp, p))
+        && max <= 20) {
+        max *= 2;
+        goto creat;
+    }
+    
+    if (APR_SUCCESS == rv) {
+        rv = write_cb(baton, f, p);
+        apr_file_close(f);
+        
+        if (APR_SUCCESS == rv) {
+            rv = apr_file_rename(tmp, fpath, p);
+            if (APR_SUCCESS != rv) {
+                apr_file_remove(tmp, p);
+            }
+        }
+    }
+    return rv;
+}                            
+
+/**************************************************************************************************/
+/* text files */
+
+apr_status_t md_text_fread8k(const char **ptext, apr_pool_t *p, const char *fpath)
+{
+    apr_status_t rv;
+    apr_file_t *f;
+    char buffer[8 * 1024];
+
+    *ptext = NULL;
+    if (APR_SUCCESS == (rv = apr_file_open(&f, fpath, APR_FOPEN_READ, 0, p))) {
+        apr_size_t blen = sizeof(buffer)/sizeof(buffer[0]) - 1;
+        rv = apr_file_read_full(f, buffer, blen, &blen);
+        if (APR_SUCCESS == rv || APR_STATUS_IS_EOF(rv)) {
+            *ptext = apr_pstrndup(p, buffer, blen);
+            rv = APR_SUCCESS;
+        }
+        apr_file_close(f);
+    }
+    return rv;
+}
+
+static apr_status_t write_text(void *baton, struct apr_file_t *f, apr_pool_t *p)
+{
+    const char *text = baton;
+    apr_size_t len = strlen(text);
+    
+    (void)p;
+    return apr_file_write_full(f, text, len, &len);
+}
+
+apr_status_t md_text_fcreatex(const char *fpath, apr_fileperms_t perms, 
+                              apr_pool_t *p, const char *text)
+{
+    apr_status_t rv;
+    apr_file_t *f;
+    
+    rv = md_util_fcreatex(&f, fpath, perms, p);
+    if (APR_SUCCESS == rv) {
+        rv = write_text((void*)text, f, p);
+        apr_file_close(f);
+    }
+    return rv;
+}
+
+apr_status_t md_text_freplace(const char *fpath, apr_fileperms_t perms, 
+                              apr_pool_t *p, const char *text)
+{
+    return md_util_freplace(fpath, perms, p, write_text, (void*)text);
+}
+
+typedef struct {
+    const char *path;
+    apr_array_header_t *patterns;
+    int follow_links;
+    void *baton;
+    md_util_fdo_cb *cb;
+} md_util_fwalk_t;
+
+static apr_status_t rm_recursive(const char *fpath, apr_pool_t *p, int max_level)
+{
+    apr_finfo_t info;
+    apr_status_t rv;
+    const char *npath;
+    
+    if (APR_SUCCESS != (rv = apr_stat(&info, fpath, (APR_FINFO_TYPE|APR_FINFO_LINK), p))) {
+        return rv;
+    }
+    
+    if (info.filetype == APR_DIR) {
+        if (max_level > 0) {
+            apr_dir_t *d;
+            
+            if (APR_SUCCESS == (rv = apr_dir_open(&d, fpath, p))) {
+            
+                while (APR_SUCCESS == rv && 
+                       APR_SUCCESS == (rv = apr_dir_read(&info, APR_FINFO_TYPE, d))) {
+                    if (!strcmp(".", info.name) || !strcmp("..", info.name)) {
+                        continue;
+                    }
+                    
+                    rv = md_util_path_merge(&npath, p, fpath, info.name, NULL);
+                    if (APR_SUCCESS == rv) {
+                        rv = rm_recursive(npath, p, max_level - 1);
+                    }
+                }
+                apr_dir_close(d);
+                if (APR_STATUS_IS_ENOENT(rv)) {
+                    rv = APR_SUCCESS;
+                }
+            }
+        }
+        if (APR_SUCCESS == rv) {
+            rv = apr_dir_remove(fpath, p);
+        }
+    }
+    else {
+        rv = apr_file_remove(fpath, p);
+    }
+    return rv;
+}
+
+static apr_status_t prm_recursive(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    int max_level = va_arg(ap, int);
+    
+    (void)p;
+    return rm_recursive(baton, ptemp, max_level); 
+}
+
+apr_status_t md_util_rm_recursive(const char *fpath, apr_pool_t *p, int max_level)
+{
+    return md_util_pool_vdo(prm_recursive, (void*)fpath, p, max_level, NULL);
+}
+
+static apr_status_t match_and_do(md_util_fwalk_t *ctx, const char *path, int depth, 
+                                 apr_pool_t *p, apr_pool_t *ptemp)
+{
+    apr_status_t rv = APR_SUCCESS;
+    const char *pattern, *npath;
+    apr_dir_t *d;
+    apr_finfo_t finfo;
+    int ndepth = depth + 1;
+    apr_int32_t wanted = (APR_FINFO_TYPE);
+
+    if (depth >= ctx->patterns->nelts) {
+        return APR_SUCCESS;
+    }
+    pattern = APR_ARRAY_IDX(ctx->patterns, depth, const char *);
+    
+    rv = apr_dir_open(&d, path, ptemp);
+    if (APR_SUCCESS != rv) {
+        return rv;
+    }
+    
+    while (APR_SUCCESS == (rv = apr_dir_read(&finfo, wanted, d))) {
+        if (!strcmp(".", finfo.name) || !strcmp("..", finfo.name)) {
+            continue;
+        } 
+        if (APR_SUCCESS == apr_fnmatch(pattern, finfo.name, 0)) {
+            if (ndepth < ctx->patterns->nelts) {
+                if (APR_DIR == finfo.filetype) { 
+                    /* deeper and deeper, irgendwo in der tiefe leuchtet ein licht */
+                    rv = md_util_path_merge(&npath, ptemp, path, finfo.name, NULL);
+                    if (APR_SUCCESS == rv) {
+                        rv = match_and_do(ctx, npath, ndepth, p, ptemp);
+                    }
+                }
+            }
+            else {
+                rv = ctx->cb(ctx->baton, p, ptemp, path, finfo.name, finfo.filetype);
+            }
+        }
+        if (APR_SUCCESS != rv) {
+            break;
+        }
+    }
+
+    if (APR_STATUS_IS_ENOENT(rv)) {
+        rv = APR_SUCCESS;
+    }
+
+    apr_dir_close(d);
+    return rv;
+}
+
+static apr_status_t files_do_start(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap)
+{
+    md_util_fwalk_t *ctx = baton;
+    const char *segment;
+
+    ctx->patterns = apr_array_make(ptemp, 5, sizeof(const char*));
+    
+    segment = va_arg(ap, char *);
+    while (segment) {
+        APR_ARRAY_PUSH(ctx->patterns, const char *) = segment;
+        segment = va_arg(ap, char *);
+    }
+    
+    return match_and_do(ctx, ctx->path, 0, p, ptemp);
+}
+
+apr_status_t md_util_files_do(md_util_fdo_cb *cb, void *baton, apr_pool_t *p,
+                              const char *path, ...)
+{
+    apr_status_t rv;
+    va_list ap;
+    md_util_fwalk_t ctx;
+
+    memset(&ctx, 0, sizeof(ctx));
+    ctx.path = path;
+    ctx.follow_links = 1;
+    ctx.cb = cb;
+    ctx.baton = baton;
+    
+    va_start(ap, path);
+    rv = pool_vado(files_do_start, &ctx, p, ap);
+    va_end(ap);
+    
+    return rv;
+}
+
+static apr_status_t tree_do(void *baton, apr_pool_t *p, apr_pool_t *ptemp, const char *path)
+{
+    md_util_fwalk_t *ctx = baton;
+
+    apr_status_t rv = APR_SUCCESS;
+    const char *name, *fpath;
+    apr_filetype_e ftype;
+    apr_dir_t *d;
+    apr_int32_t wanted = APR_FINFO_TYPE;
+    apr_finfo_t finfo;
+
+    if (APR_SUCCESS == (rv = apr_dir_open(&d, path, ptemp))) {
+        while (APR_SUCCESS == (rv = apr_dir_read(&finfo, wanted, d))) {
+            name = finfo.name;
+            if (!strcmp(".", name) || !strcmp("..", name)) {
+                continue;
+            }
+
+            fpath = NULL;
+            ftype = finfo.filetype;
+            
+            if (APR_LNK == ftype && ctx->follow_links) {
+                rv = md_util_path_merge(&fpath, ptemp, path, name, NULL);
+                if (APR_SUCCESS == rv) {
+                    rv = apr_stat(&finfo, ctx->path, wanted, ptemp);
+                }
+            }
+            
+            if (APR_DIR == finfo.filetype) {
+                if (!fpath) {
+                    rv = md_util_path_merge(&fpath, ptemp, path, name, NULL);
+                }
+                if (APR_SUCCESS == rv) {
+                    rv = tree_do(ctx, p, ptemp, fpath);
+                    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, ptemp, "dir cb(%s/%s)", 
+                                  path, name);
+                    rv = ctx->cb(ctx->baton, p, ptemp, path, name, ftype);
+                }
+            }
+            else {
+                md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, rv, ptemp, "file cb(%s/%s)", 
+                              path, name);
+                rv = ctx->cb(ctx->baton, p, ptemp, path, name, finfo.filetype);
+            }
+        }
+
+        apr_dir_close(d);
+        
+        if (APR_STATUS_IS_ENOENT(rv)) {
+            rv = APR_SUCCESS;
+        }
+    }
+    return rv;
+}
+
+static apr_status_t tree_start_do(void *baton, apr_pool_t *p, apr_pool_t *ptemp)
+{
+    md_util_fwalk_t *ctx = baton;
+    apr_finfo_t info;
+    apr_status_t rv;
+    apr_int32_t wanted = ctx->follow_links? APR_FINFO_TYPE : (APR_FINFO_TYPE|APR_FINFO_LINK);
+    
+    rv = apr_stat(&info, ctx->path, wanted, ptemp);
+    if (rv == APR_SUCCESS) {
+        switch (info.filetype) {
+            case APR_DIR:
+                rv = tree_do(ctx, p, ptemp, ctx->path);
+                break;
+            default:
+                rv = APR_EINVAL;
+        }
+    }
+    return rv;
+}
+
+apr_status_t md_util_tree_do(md_util_fdo_cb *cb, void *baton, apr_pool_t *p, 
+                             const char *path, int follow_links)
+{
+    apr_status_t rv;
+    md_util_fwalk_t ctx;
+
+    memset(&ctx, 0, sizeof(ctx));
+    ctx.path = path;
+    ctx.follow_links = follow_links;
+    ctx.cb = cb;
+    ctx.baton = baton;
+    
+    rv = md_util_pool_do(tree_start_do, &ctx, p);
+    
+    return rv;
+}
+
+static apr_status_t rm_cb(void *baton, apr_pool_t *p, apr_pool_t *ptemp, 
+                          const char *path, const char *name, apr_filetype_e ftype)
+{
+    apr_status_t rv;
+    const char *fpath;
+    
+    (void)baton;
+    (void)p;
+    rv = md_util_path_merge(&fpath, ptemp, path, name, NULL);
+    if (APR_SUCCESS == rv) {
+        if (APR_DIR == ftype) {
+            rv = apr_dir_remove(fpath, ptemp);
+        }
+        else {
+            rv = apr_file_remove(fpath, ptemp);
+        }
+    }
+    return rv;
+}
+
+apr_status_t md_util_ftree_remove(const char *path, apr_pool_t *p)
+{
+    apr_status_t rv = md_util_tree_do(rm_cb, NULL, p, path, 0);
+    if (APR_SUCCESS == rv) {
+        rv = apr_dir_remove(path, p);
+    }
+    return rv;
+}
+
+/* DNS name checks ********************************************************************************/
+
+int md_util_is_dns_name(apr_pool_t *p, const char *hostname, int need_fqdn)
+{
+    char c, last = 0;
+    const char *cp = hostname;
+    int dots = 0;
+    
+    /* Since we use the names in certificates, we need pure ASCII domain names
+     * and IDN need to be converted to unicode. */
+    while ((c = *cp++)) {
+        switch (c) {
+            case '.':
+                if (last == '.') {
+                    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "dns name with ..: %s", 
+                                  hostname);
+                    return 0;
+                }
+                ++dots;
+                break;
+            case '-':
+                break;
+            default:
+                if (!apr_isalnum(c)) {
+                    md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "dns invalid char %c: %s", 
+                                  c, hostname);
+                    return 0;
+                }
+                break;
+        }
+        last = c;
+    }
+    
+    if (last == '.') { /* DNS names may end with '.' */
+        --dots;
+    }
+    if (need_fqdn && dots <= 0) { /* do not accept just top level domains */
+        md_log_perror(MD_LOG_MARK, MD_LOG_TRACE3, 0, p, "not a FQDN: %s", hostname);
+        return 0;
+    }
+    return 1; /* empty string not allowed */
+}
+
+const char *md_util_schemify(apr_pool_t *p, const char *s, const char *def_scheme)
+{
+    const char *cp = s;
+    while (*cp) {
+        if (*cp == ':') {
+            /* could be an url scheme, leave unchanged */
+            return s;
+        }
+        else if (!apr_isalnum(*cp)) {
+            break;
+        }
+        ++cp;
+    }
+    return apr_psprintf(p, "%s:%s", def_scheme, s);
+}
+
+static apr_status_t uri_check(apr_uri_t *uri_parsed, apr_pool_t *p, 
+                              const char *uri, const char **perr)
+{
+    const char *s, *err = NULL;
+    apr_status_t rv;
+    
+    if (APR_SUCCESS != (rv = apr_uri_parse(p, uri, uri_parsed))) {
+        err = "not an uri";
+    }
+    else if (uri_parsed->scheme) {
+        if (strlen(uri_parsed->scheme) + 1 >= strlen(uri)) {
+            err = "missing uri identifier";
+        }
+        else if (!strncmp("http", uri_parsed->scheme, 4)) {
+            if (!uri_parsed->hostname) {
+                err = "missing hostname";
+            }
+            else if (!md_util_is_dns_name(p, uri_parsed->hostname, 0)) {
+                err = "invalid hostname";
+            }
+            if (uri_parsed->port_str 
+                && (!apr_isdigit(uri_parsed->port_str[0])
+                || uri_parsed->port == 0
+                || uri_parsed->port > 65353)) {
+                err = "invalid port";
+            }
+        }
+        else if (!strcmp("mailto", uri_parsed->scheme)) {
+            s = strchr(uri, '@');
+            if (!s) {
+                err = "missing @";
+            }
+            else if (strchr(s+1, '@')) {
+                err = "duplicate @";
+            }
+            else if (s == uri + strlen(uri_parsed->scheme) + 1) {
+                err = "missing local part";
+            }
+            else if (s == (uri + strlen(uri)-1)) {
+                err = "missing hostname";
+            }
+            else if (strstr(uri, "..")) {
+                err = "double period";
+            }
+        }
+    }
+    if (strchr(uri, ' ') || strchr(uri, '\t') ) {
+        err = "whitespace in uri";
+    }
+    
+    if (err) {
+        rv = APR_EINVAL;
+    }
+    *perr = err;
+    return rv;
+}
+
+apr_status_t md_util_abs_uri_check(apr_pool_t *p, const char *uri, const char **perr)
+{
+    apr_uri_t uri_parsed;
+    apr_status_t rv;
+
+    if (APR_SUCCESS == (rv = uri_check(&uri_parsed, p, uri, perr))) {
+        if (!uri_parsed.scheme) {
+            *perr = "missing uri scheme";
+            return APR_EINVAL;
+        }
+    }
+    return rv;
+}
+
+apr_status_t md_util_abs_http_uri_check(apr_pool_t *p, const char *uri, const char **perr)
+{
+    apr_uri_t uri_parsed;
+    apr_status_t rv;
+
+    if (APR_SUCCESS == (rv = uri_check(&uri_parsed, p, uri, perr))) {
+        if (!uri_parsed.scheme) {
+            *perr = "missing uri scheme";
+            return APR_EINVAL;
+        }
+        if (apr_strnatcasecmp("http", uri_parsed.scheme) 
+            && apr_strnatcasecmp("https", uri_parsed.scheme)) {
+            *perr = "uri scheme must be http or https";
+            return APR_EINVAL;
+        }
+    }
+    return rv;
+}
+
+/* try and retry for a while **********************************************************************/
+
+apr_status_t md_util_try(md_util_try_fn *fn, void *baton, int ignore_errs, 
+                         apr_interval_time_t timeout, apr_interval_time_t start_delay, 
+                         apr_interval_time_t max_delay, int backoff)
+{
+    apr_status_t rv;
+    apr_time_t now = apr_time_now();
+    apr_time_t giveup = now + timeout;
+    apr_interval_time_t nap_duration = start_delay? start_delay : apr_time_from_msec(100);
+    apr_interval_time_t nap_max = max_delay? max_delay : apr_time_from_sec(10);
+    apr_interval_time_t left;
+    int i = 0;
+    
+    while (1) {
+        if (APR_SUCCESS == (rv = fn(baton, i++))) {
+            break;
+        }
+        else if (!APR_STATUS_IS_EAGAIN(rv) && !ignore_errs) {
+            break;
+        }
+        
+        now = apr_time_now();
+        if (now > giveup) {
+            rv = APR_TIMEUP;
+            break;
+        }
+        
+        left = giveup - now;
+        if (nap_duration > left) {
+            nap_duration = left;
+        }
+        if (nap_duration > nap_max) {
+            nap_duration = nap_max;
+        }
+        
+        apr_sleep(nap_duration);
+        if (backoff) {
+            nap_duration *= 2;
+        } 
+    }
+    return rv;
+}
+
+/* execute process ********************************************************************************/
+
+apr_status_t md_util_exec(apr_pool_t *p, const char *cmd, const char * const *argv,
+                          int *exit_code)
+{
+    apr_status_t rv;
+    apr_procattr_t *procattr;
+    apr_proc_t *proc;
+    apr_exit_why_e ewhy;
+
+    *exit_code = 0;
+    if (!(proc = apr_pcalloc(p, sizeof(*proc)))) {
+        return APR_ENOMEM;
+    }
+    if (   APR_SUCCESS == (rv = apr_procattr_create(&procattr, p))
+        && APR_SUCCESS == (rv = apr_procattr_io_set(procattr, APR_NO_FILE, 
+                                                    APR_NO_PIPE, APR_NO_PIPE))
+        && APR_SUCCESS == (rv = apr_procattr_cmdtype_set(procattr, APR_PROGRAM))
+        && APR_SUCCESS == (rv = apr_proc_create(proc, cmd, argv, NULL, procattr, p))
+        && APR_CHILD_DONE == (rv = apr_proc_wait(proc, exit_code, &ewhy, APR_WAIT))) {
+        /* let's not dwell on exit stati, but core should signal something's bad */
+        if (*exit_code > 127 || APR_PROC_SIGNAL_CORE == ewhy) {
+            return APR_EINCOMPLETE;
+        }
+        return APR_SUCCESS;
+    }
+    return rv;
+}
+
+
+/* date/time encoding *****************************************************************************/
+
+const char *md_print_duration(apr_pool_t *p, apr_interval_time_t duration)
+{
+    int secs = (int)(apr_time_sec(duration) % MD_SECS_PER_DAY);
+    return apr_psprintf(p, "%2d:%02d:%02d hours", 
+                        (int)secs/MD_SECS_PER_HOUR, (int)(secs%(MD_SECS_PER_HOUR))/60,
+                        (int)(secs%60));
+}
+
+
+/* base64 url encoding ****************************************************************************/
+
+#define N6 (unsigned int)-1
+
+static const unsigned int BASE64URL_UINT6[] = {
+/*   0   1   2   3   4   5   6   7   8   9   a   b   c   d   e   f        */
+    N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /*  0 */
+    N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /*  1 */ 
+    N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, 62, N6, N6, /*  2 */
+    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, N6, N6, N6, N6, N6, N6, /*  3 */ 
+    N6, 0,  1,  2,  3,  4,  5,  6,   7,  8,  9, 10, 11, 12, 13, 14, /*  4 */
+    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, N6, N6, N6, N6, 63, /*  5 */
+    N6, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, /*  6 */
+    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, N6, N6, N6, N6, N6, /*  7 */
+    N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /*  8 */
+    N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /*  9 */
+    N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /*  a */
+    N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /*  b */
+    N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /*  c */
+    N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /*  d */
+    N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, /*  e */
+    N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6, N6  /*  f */
+};
+static const unsigned char BASE64URL_CHARS[] = {
+    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', /*  0 -  9 */
+    'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', /* 10 - 19 */
+    'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', /* 20 - 29 */
+    'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', /* 30 - 39 */
+    'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', /* 40 - 49 */
+    'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', /* 50 - 59 */
+    '8', '9', '-', '_', ' ', ' ', ' ', ' ', ' ', ' ', /* 60 - 69 */
+};
+
+#define BASE64URL_CHAR(x)    BASE64URL_CHARS[ (unsigned int)(x) & 0x3fu ]
+   
+apr_size_t md_util_base64url_decode(const char **decoded, const char *encoded, 
+                                    apr_pool_t *pool)
+{
+    const unsigned char *e = (const unsigned char *)encoded;
+    const unsigned char *p = e;
+    unsigned char *d;
+    unsigned int n;
+    long len, mlen, remain, i;
+    
+    while (*p && BASE64URL_UINT6[ *p ] != N6) {
+        ++p;
+    }
+    len = (int)(p - e);
+    mlen = (len/4)*4;
+    *decoded = apr_pcalloc(pool, (apr_size_t)len + 1);
+    
+    i = 0;
+    d = (unsigned char*)*decoded;
+    for (; i < mlen; i += 4) {
+        n = ((BASE64URL_UINT6[ e[i+0] ] << 18) +
+             (BASE64URL_UINT6[ e[i+1] ] << 12) +
+             (BASE64URL_UINT6[ e[i+2] ] << 6) +
+             (BASE64URL_UINT6[ e[i+3] ]));
+        *d++ = (unsigned char)(n >> 16);
+        *d++ = (unsigned char)(n >> 8 & 0xffu);
+        *d++ = (unsigned char)(n & 0xffu);
+    }
+    remain = len - mlen;
+    switch (remain) {
+        case 2:
+            n = ((BASE64URL_UINT6[ e[mlen+0] ] << 18) +
+                 (BASE64URL_UINT6[ e[mlen+1] ] << 12));
+            *d++ = (unsigned char)(n >> 16);
+            remain = 1;
+            break;
+        case 3:
+            n = ((BASE64URL_UINT6[ e[mlen+0] ] << 18) +
+                 (BASE64URL_UINT6[ e[mlen+1] ] << 12) +
+                 (BASE64URL_UINT6[ e[mlen+2] ] << 6));
+            *d++ = (unsigned char)(n >> 16);
+            *d++ = (unsigned char)(n >> 8 & 0xffu);
+            remain = 2;
+            break;
+        default: /* do nothing */
+            break;
+    }
+    return (apr_size_t)(mlen/4*3 + remain);
+}
+
+const char *md_util_base64url_encode(const char *data, apr_size_t dlen, apr_pool_t *pool)
+{
+    int i, len = (int)dlen;
+    apr_size_t slen = ((dlen+2)/3)*4 + 1; /* 0 terminated */
+    const unsigned char *udata = (const unsigned char*)data;
+    unsigned char *enc, *p = apr_pcalloc(pool, slen);
+    
+    enc = p;
+    for (i = 0; i < len-2; i+= 3) {
+        *p++ = BASE64URL_CHAR( (udata[i]   >> 2) );
+        *p++ = BASE64URL_CHAR( (udata[i]   << 4) + (udata[i+1] >> 4) );
+        *p++ = BASE64URL_CHAR( (udata[i+1] << 2) + (udata[i+2] >> 6) );
+        *p++ = BASE64URL_CHAR( (udata[i+2]) );
+    }
+    
+    if (i < len) {
+        *p++ = BASE64URL_CHAR( (udata[i] >> 2) );
+        if (i == (len - 1)) {
+            *p++ = BASE64URL_CHARS[ ((unsigned int)udata[i] << 4) & 0x3fu ];
+        }
+        else {
+            *p++ = BASE64URL_CHAR( (udata[i] << 4) + (udata[i+1] >> 4) );
+            *p++ = BASE64URL_CHAR( (udata[i+1] << 2) );
+        }
+    }
+    *p++ = '\0';
+    return (char *)enc;
+}
+
+/*******************************************************************************
+ * link header handling 
+ ******************************************************************************/
+
+typedef struct {
+    const char *s;
+    apr_size_t slen;
+    apr_size_t i;
+    apr_size_t link_start;
+    apr_size_t link_len;
+    apr_size_t pn_start;
+    apr_size_t pn_len;
+    apr_size_t pv_start;
+    apr_size_t pv_len;
+} link_ctx;
+
+static int attr_char(char c) 
+{
+    switch (c) {
+        case '!':
+        case '#':
+        case '$':
+        case '&':
+        case '+':
+        case '-':
+        case '.':
+        case '^':
+        case '_':
+        case '`':
+        case '|':
+        case '~':
+            return 1;
+        default:
+            return apr_isalnum(c);
+    }
+}
+
+static int ptoken_char(char c) 
+{
+    switch (c) {
+        case '!':
+        case '#':
+        case '$':
+        case '&':
+        case '\'':
+        case '(':
+        case ')':
+        case '*':
+        case '+':
+        case '-':
+        case '.':
+        case '/':
+        case ':':
+        case '<':
+        case '=':
+        case '>':
+        case '?':
+        case '@':
+        case '[':
+        case ']':
+        case '^':
+        case '_':
+        case '`':
+        case '{':
+        case '|':
+        case '}':
+        case '~':
+            return 1;
+        default:
+            return apr_isalnum(c);
+    }
+}
+
+static int skip_ws(link_ctx *ctx)
+{
+    char c;
+    while (ctx->i < ctx->slen 
+           && (((c = ctx->s[ctx->i]) == ' ') || (c == '\t'))) {
+        ++ctx->i;
+    }
+    return (ctx->i < ctx->slen);
+}
+
+static int skip_nonws(link_ctx *ctx)
+{
+    char c;
+    while (ctx->i < ctx->slen 
+           && (((c = ctx->s[ctx->i]) != ' ') && (c != '\t'))) {
+        ++ctx->i;
+    }
+    return (ctx->i < ctx->slen);
+}
+
+static unsigned int find_chr(link_ctx *ctx, char c, apr_size_t *pidx)
+{
+    apr_size_t j;
+    for (j = ctx->i; j < ctx->slen; ++j) {
+        if (ctx->s[j] == c) {
+            *pidx = j;
+            return 1;
+        }
+    } 
+    return 0;
+}
+
+static int read_chr(link_ctx *ctx, char c)
+{
+    if (ctx->i < ctx->slen && ctx->s[ctx->i] == c) {
+        ++ctx->i;
+        return 1;
+    }
+    return 0;
+}
+
+static int skip_qstring(link_ctx *ctx)
+{
+    if (skip_ws(ctx) && read_chr(ctx, '\"')) {
+        apr_size_t end;
+        if (find_chr(ctx, '\"', &end)) {
+            ctx->i = end + 1;
+            return 1;
+        }
+    }
+    return 0;
+}
+
+static int skip_ptoken(link_ctx *ctx)
+{
+    if (skip_ws(ctx)) {
+        apr_size_t i;
+        for (i = ctx->i; i < ctx->slen && ptoken_char(ctx->s[i]); ++i) {
+            /* nop */
+        }
+        if (i > ctx->i) {
+            ctx->i = i;
+            return 1;
+        }
+    }
+    return 0;
+}
+
+
+static int read_link(link_ctx *ctx)
+{
+    ctx->link_start = ctx->link_len = 0;
+    if (skip_ws(ctx) && read_chr(ctx, '<')) {
+        apr_size_t end;
+        if (find_chr(ctx, '>', &end)) {
+            ctx->link_start = ctx->i;
+            ctx->link_len = end - ctx->link_start;
+            ctx->i = end + 1;
+            return 1;
+        }
+    }
+    return 0;
+}
+
+static int skip_pname(link_ctx *ctx)
+{
+    if (skip_ws(ctx)) {
+        apr_size_t i;
+        for (i = ctx->i; i < ctx->slen && attr_char(ctx->s[i]); ++i) {
+            /* nop */
+        }
+        if (i > ctx->i) {
+            ctx->i = i;
+            return 1;
+        }
+    }
+    return 0;
+}
+
+static int skip_pvalue(link_ctx *ctx)
+{
+    if (skip_ws(ctx) && read_chr(ctx, '=')) {
+        ctx->pv_start = ctx->i;
+        if (skip_qstring(ctx) || skip_ptoken(ctx)) {
+            ctx->pv_len = ctx->i - ctx->pv_start;
+            return 1;
+        }
+    }
+    return 0;
+}
+
+static int skip_param(link_ctx *ctx)
+{
+    if (skip_ws(ctx) && read_chr(ctx, ';')) {
+        ctx->pn_start = ctx->i;
+        ctx->pn_len = 0;
+        if (skip_pname(ctx)) {
+            ctx->pn_len = ctx->i - ctx->pn_start;
+            ctx->pv_len = 0;
+            skip_pvalue(ctx); /* value is optional */
+            return 1;
+        }
+    }
+    return 0;
+}
+
+static int pv_contains(link_ctx *ctx, const char *s)
+{
+    apr_size_t pvstart = ctx->pv_start;
+    apr_size_t pvlen = ctx->pv_len;
+    
+    if (ctx->s[pvstart] == '\"' && pvlen > 1) {
+        ++pvstart;
+        pvlen -= 2;
+    }
+    if (pvlen > 0) {
+        apr_size_t slen = strlen(s);
+        link_ctx pvctx;
+        apr_size_t i;
+        
+        memset(&pvctx, 0, sizeof(pvctx));
+        pvctx.s = ctx->s + pvstart;
+        pvctx.slen = pvlen;
+
+        for (i = 0; i < pvctx.slen; i = pvctx.i) {
+            skip_nonws(&pvctx);
+            if ((pvctx.i - i) == slen && !strncmp(s, pvctx.s + i, slen)) {
+                return 1;
+            }
+            skip_ws(&pvctx);
+        }
+    }
+    return 0;
+}
+
+/* RFC 5988 <https://tools.ietf.org/html/rfc5988#section-6.2.1>
+  Link           = "Link" ":" #link-value
+  link-value     = "<" URI-Reference ">" *( ";" link-param )
+  link-param     = ( ( "rel" "=" relation-types )
+                 | ( "anchor" "=" <"> URI-Reference <"> )
+                 | ( "rev" "=" relation-types )
+                 | ( "hreflang" "=" Language-Tag )
+                 | ( "media" "=" ( MediaDesc | ( <"> MediaDesc <"> ) ) )
+                 | ( "title" "=" quoted-string )
+                 | ( "title*" "=" ext-value )
+                 | ( "type" "=" ( media-type | quoted-mt ) )
+                 | ( link-extension ) )
+  link-extension = ( parmname [ "=" ( ptoken | quoted-string ) ] )
+                 | ( ext-name-star "=" ext-value )
+  ext-name-star  = parmname "*" ; reserved for RFC2231-profiled
+                                ; extensions.  Whitespace NOT
+                                ; allowed in between.
+  ptoken         = 1*ptokenchar
+  ptokenchar     = "!" | "#" | "$" | "%" | "&" | "'" | "("
+                 | ")" | "*" | "+" | "-" | "." | "/" | DIGIT
+                 | ":" | "<" | "=" | ">" | "?" | "@" | ALPHA
+                 | "[" | "]" | "^" | "_" | "`" | "{" | "|"
+                 | "}" | "~"
+  media-type     = type-name "/" subtype-name
+  quoted-mt      = <"> media-type <">
+  relation-types = relation-type
+                 | <"> relation-type *( 1*SP relation-type ) <">
+  relation-type  = reg-rel-type | ext-rel-type
+  reg-rel-type   = LOALPHA *( LOALPHA | DIGIT | "." | "-" )
+  ext-rel-type   = URI
+  
+  and from <https://tools.ietf.org/html/rfc5987>
+  parmname      = 1*attr-char
+  attr-char     = ALPHA / DIGIT
+                   / "!" / "#" / "$" / "&" / "+" / "-" / "."
+                   / "^" / "_" / "`" / "|" / "~"
+ */
+
+typedef struct {
+    apr_pool_t *pool;
+    const char *relation;
+    const char *url;
+} find_ctx;
+
+static int find_url(void *baton, const char *key, const char *value)
+{
+    find_ctx *outer = baton;
+    
+    if (!apr_strnatcasecmp("link", key)) {
+        link_ctx ctx;
+        
+        memset(&ctx, 0, sizeof(ctx));
+        ctx.s = value;
+        ctx.slen = strlen(value);
+        
+        while (read_link(&ctx)) {
+            while (skip_param(&ctx)) {
+                if (ctx.pn_len == 3 && !strncmp("rel", ctx.s + ctx.pn_start, 3)
+                    && pv_contains(&ctx, outer->relation)) {
+                    /* this is the link relation we are looking for */
+                    outer->url = apr_pstrndup(outer->pool, ctx.s + ctx.link_start, ctx.link_len);
+                    return 0;
+                }
+            }
+        }
+    }
+    return 1;
+}
+
+const char *md_link_find_relation(const apr_table_t *headers, 
+                                  apr_pool_t *pool, const char *relation)
+{
+    find_ctx ctx;
+    
+    memset(&ctx, 0, sizeof(ctx));
+    ctx.pool = pool;
+    ctx.relation = relation;
+    
+    apr_table_do(find_url, &ctx, headers, NULL);
+    
+    return ctx.url;
+}
+
diff --git a/modules/md/md_util.h b/modules/md/md_util.h
new file mode 100644 (file)
index 0000000..a69635b
--- /dev/null
@@ -0,0 +1,147 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef mod_md_md_util_h
+#define mod_md_md_util_h
+
+#include <stdio.h>
+#include <apr_file_io.h>
+
+struct apr_array_header_t;
+struct apr_table_t;
+
+/**************************************************************************************************/
+/* pool utils */
+
+typedef apr_status_t md_util_action(void *baton, apr_pool_t *p, apr_pool_t *ptemp);
+typedef apr_status_t md_util_vaction(void *baton, apr_pool_t *p, apr_pool_t *ptemp, va_list ap);
+
+apr_status_t md_util_pool_do(md_util_action *cb, void *baton, apr_pool_t *p); 
+apr_status_t md_util_pool_vdo(md_util_vaction *cb, void *baton, apr_pool_t *p, ...); 
+
+/**************************************************************************************************/
+/* string related */
+char *md_util_str_tolower(char *s);
+
+int md_array_str_index(const struct apr_array_header_t *array, const char *s, 
+                       int start, int case_sensitive);
+
+int md_array_str_eq(const struct apr_array_header_t *a1, 
+                    const struct apr_array_header_t *a2, int case_sensitive);
+
+struct apr_array_header_t *md_array_str_clone(apr_pool_t *p, struct apr_array_header_t *array);
+
+struct apr_array_header_t *md_array_str_compact(apr_pool_t *p, struct apr_array_header_t *src,
+                                                int case_sensitive);
+
+struct apr_array_header_t *md_array_str_remove(apr_pool_t *p, struct apr_array_header_t *src, 
+                                               const char *exclude, int case_sensitive);
+
+int md_array_str_add_missing(struct apr_array_header_t *dest, 
+                             struct apr_array_header_t *src, int case_sensitive);
+
+/**************************************************************************************************/
+/* process execution */
+apr_status_t md_util_exec(apr_pool_t *p, const char *cmd, const char * const *argv,
+                          int *exit_code);
+
+/**************************************************************************************************/
+/* dns name check */
+
+int md_util_is_dns_name(apr_pool_t *p, const char *hostname, int need_fqdn);
+
+/**************************************************************************************************/
+/* file system related */
+
+struct apr_file_t;
+struct apr_finfo_t;
+
+apr_status_t md_util_fopen(FILE **pf, const char *fn, const char *mode);
+
+apr_status_t md_util_fcreatex(struct apr_file_t **pf, const char *fn, 
+                              apr_fileperms_t perms, apr_pool_t *p);
+
+apr_status_t md_util_path_merge(const char **ppath, apr_pool_t *p, ...);
+
+apr_status_t md_util_is_dir(const char *path, apr_pool_t *pool);
+apr_status_t md_util_is_file(const char *path, apr_pool_t *pool);
+
+typedef apr_status_t md_util_file_cb(void *baton, struct apr_file_t *f, apr_pool_t *p);
+
+apr_status_t md_util_freplace(const char *fpath, apr_fileperms_t perms, apr_pool_t *p, 
+                              md_util_file_cb *write, void *baton);
+
+/** 
+ * Remove a file/directory and all files/directories contain up to max_level. If max_level == 0,
+ * only an empty directory or a file can be removed.
+ */
+apr_status_t md_util_rm_recursive(const char *fpath, apr_pool_t *p, int max_level);
+
+typedef apr_status_t md_util_fdo_cb(void *baton, apr_pool_t *p, apr_pool_t *ptemp, 
+                                         const char *dir, const char *name, 
+                                         apr_filetype_e ftype);
+                                         
+apr_status_t md_util_files_do(md_util_fdo_cb *cb, void *baton, apr_pool_t *p, 
+                              const char *path, ...);
+
+/**
+ * Depth first traversal of directory tree starting at path.
+ */
+apr_status_t md_util_tree_do(md_util_fdo_cb *cb, void *baton, apr_pool_t *p, 
+                             const char *path, int follow_links);
+
+apr_status_t md_util_ftree_remove(const char *path, apr_pool_t *p);
+
+apr_status_t md_text_fread8k(const char **ptext, apr_pool_t *p, const char *fpath);
+apr_status_t md_text_fcreatex(const char *fpath, apr_fileperms_t 
+                              perms, apr_pool_t *p, const char *text);
+apr_status_t md_text_freplace(const char *fpath, apr_fileperms_t perms, 
+                              apr_pool_t *p, const char *text); 
+
+/**************************************************************************************************/
+/* base64 url encodings */
+const char *md_util_base64url_encode(const char *data, 
+                                     apr_size_t len, apr_pool_t *pool);
+apr_size_t md_util_base64url_decode(const char **decoded, const char *encoded, 
+                                    apr_pool_t *pool);
+
+/**************************************************************************************************/
+/* http/url related */
+const char *md_util_schemify(apr_pool_t *p, const char *s, const char *def_scheme);
+
+apr_status_t md_util_abs_uri_check(apr_pool_t *p, const char *s, const char **perr);
+apr_status_t md_util_abs_http_uri_check(apr_pool_t *p, const char *uri, const char **perr);
+
+const char *md_link_find_relation(const struct apr_table_t *headers, 
+                                  apr_pool_t *pool, const char *relation);
+
+/**************************************************************************************************/
+/* retry logic */
+
+typedef apr_status_t md_util_try_fn(void *baton, int i);
+
+apr_status_t md_util_try(md_util_try_fn *fn, void *baton, int ignore_errs,  
+                         apr_interval_time_t timeout, apr_interval_time_t start_delay, 
+                         apr_interval_time_t max_delay, int backoff);
+
+/**************************************************************************************************/
+/* date/time related */
+
+#define MD_SECS_PER_HOUR      (60*60)
+#define MD_SECS_PER_DAY       (24*MD_SECS_PER_HOUR)
+
+const char *md_print_duration(apr_pool_t *p, apr_interval_time_t duration);
+
+#endif /* md_util_h */
diff --git a/modules/md/md_version.h b/modules/md/md_version.h
new file mode 100644 (file)
index 0000000..2e50abf
--- /dev/null
@@ -0,0 +1,42 @@
+/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de)
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+#ifndef mod_md_md_version_h
+#define mod_md_md_version_h
+
+#undef PACKAGE_VERSION
+#undef PACKAGE_TARNAME
+#undef PACKAGE_STRING
+#undef PACKAGE_NAME
+#undef PACKAGE_BUGREPORT
+
+/**
+ * @macro
+ * Version number of the md module as c string
+ */
+#define MOD_MD_VERSION "1.0.4"
+
+/**
+ * @macro
+ * Numerical representation of the version number of the md module
+ * release. This is a 24 bit number with 8 bits for major number, 8 bits
+ * for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203.
+ */
+#define MOD_MD_VERSION_NUM 0x010004
+
+#define MD_EXPERIMENTAL 0
+#define MD_ACME_DEF_URL    "https://acme-v01.api.letsencrypt.org/directory"
+
+#endif /* mod_md_md_version_h */
diff --git a/modules/md/mod_md.c b/modules/md/mod_md.c
new file mode 100644 (file)
index 0000000..87121a8
--- /dev/null
@@ -0,0 +1,1386 @@
+/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+#include <apr_optional.h>
+#include <apr_strings.h>
+
+#include <ap_release.h>
+#ifndef AP_ENABLE_EXCEPTION_HOOK
+#define AP_ENABLE_EXCEPTION_HOOK 0
+#endif
+#include <mpm_common.h>
+#include <httpd.h>
+#include <http_core.h>
+#include <http_protocol.h>
+#include <http_request.h>
+#include <http_log.h>
+#include <http_vhost.h>
+#include <ap_listen.h>
+
+#include "md.h"
+#include "md_curl.h"
+#include "md_crypt.h"
+#include "md_http.h"
+#include "md_json.h"
+#include "md_store.h"
+#include "md_store_fs.h"
+#include "md_log.h"
+#include "md_reg.h"
+#include "md_util.h"
+#include "md_version.h"
+#include "md_acme.h"
+#include "md_acme_authz.h"
+
+#include "mod_md.h"
+#include "mod_md_config.h"
+#include "mod_md_os.h"
+#include "mod_ssl.h"
+#include "mod_watchdog.h"
+
+static void md_hooks(apr_pool_t *pool);
+
+AP_DECLARE_MODULE(md) = {
+    STANDARD20_MODULE_STUFF,
+    NULL,                 /* func to create per dir config */
+    NULL,                 /* func to merge per dir config */
+    md_config_create_svr, /* func to create per server config */
+    md_config_merge_svr,  /* func to merge per server config */
+    md_cmds,              /* command handlers */
+    md_hooks,
+#if defined(AP_MODULE_FLAG_NONE)
+    AP_MODULE_FLAG_ALWAYS_MERGE
+#endif
+};
+
+static void md_merge_srv(md_t *md, md_srv_conf_t *base_sc, apr_pool_t *p)
+{
+    if (!md->sc) {
+        md->sc = base_sc;
+    }
+
+    if (!md->ca_url) {
+        md->ca_url = md_config_gets(md->sc, MD_CONFIG_CA_URL);
+    }
+    if (!md->ca_proto) {
+        md->ca_proto = md_config_gets(md->sc, MD_CONFIG_CA_PROTO);
+    }
+    if (!md->ca_agreement) {
+        md->ca_agreement = md_config_gets(md->sc, MD_CONFIG_CA_AGREEMENT);
+    }
+    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");
+    }
+    if (md->drive_mode == MD_DRIVE_DEFAULT) {
+        md->drive_mode = md_config_geti(md->sc, MD_CONFIG_DRIVE_MODE);
+    }
+    if (md->renew_norm <= 0 && md->renew_window <= 0) {
+        md->renew_norm = md_config_get_interval(md->sc, MD_CONFIG_RENEW_NORM);
+        md->renew_window = md_config_get_interval(md->sc, MD_CONFIG_RENEW_WINDOW);
+    }
+    if (md->transitive < 0) {
+        md->transitive = md_config_geti(md->sc, MD_CONFIG_TRANSITIVE);
+    }
+    if (!md->ca_challenges && md->sc->ca_challenges) {
+        md->ca_challenges = apr_array_copy(p, md->sc->ca_challenges);
+    }        
+    if (!md->pkey_spec) {
+        md->pkey_spec = md->sc->pkey_spec;
+        
+    }
+    if (md->require_https < 0) {
+        md->require_https = md_config_geti(md->sc, MD_CONFIG_REQUIRE_HTTPS);
+    }
+    if (md->must_staple < 0) {
+        md->must_staple = md_config_geti(md->sc, MD_CONFIG_MUST_STAPLE);
+    }
+}
+
+static apr_status_t check_coverage(md_t *md, const char *domain, server_rec *s, apr_pool_t *p)
+{
+    if (md_contains(md, domain, 0)) {
+        return APR_SUCCESS;
+    }
+    else if (md->transitive) {
+        APR_ARRAY_PUSH(md->domains, const char*) = apr_pstrdup(p, domain);
+        return APR_SUCCESS;
+    }
+    else {
+        ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, APLOGNO(10040)
+                     "Virtual Host %s:%d matches Managed Domain '%s', but the "
+                     "name/alias %s itself is not managed. A requested MD certificate "
+                     "will not match ServerName.",
+                     s->server_hostname, s->port, md->name, domain);
+        return APR_EINVAL;
+    }
+}
+
+static apr_status_t md_covers_server(md_t *md, server_rec *s, apr_pool_t *p)
+{
+    apr_status_t rv;
+    const char *name;
+    int i;
+    
+    if (APR_SUCCESS == (rv = check_coverage(md, s->server_hostname, s, p)) && s->names) {
+        for (i = 0; i < s->names->nelts; ++i) {
+            name = APR_ARRAY_IDX(s->names, i, const char*);
+            if (APR_SUCCESS != (rv = check_coverage(md, name, s, p))) {
+                break;
+            }
+        }
+    }
+    return rv;
+}
+
+static int matches_port_somewhere(server_rec *s, int port)
+{
+    server_addr_rec *sa;
+    
+    for (sa = s->addrs; sa; sa = sa->next) {
+        if (sa->host_port == port) {
+            /* host_addr might be general (0.0.0.0) or specific, we count this as match */
+            return 1;
+        }
+        if (sa->host_port == 0) {
+            /* wildcard port, answers to all ports. Rare, but may work. */
+            return 1;
+        }
+    }
+    return 0;
+}
+
+static int uses_port_only(server_rec *s, int port)
+{
+    server_addr_rec *sa;
+    int match = 0;
+    for (sa = s->addrs; sa; sa = sa->next) {
+        if (sa->host_port == port) {
+            /* host_addr might be general (0.0.0.0) or specific, we count this as match */
+            match = 1;
+        }
+        else {
+            /* uses other port/wildcard */
+            return 0;
+        }
+    }
+    return match;
+}
+
+static apr_status_t assign_to_servers(md_t *md, server_rec *base_server, 
+                                     apr_pool_t *p, apr_pool_t *ptemp)
+{
+    server_rec *s, *s_https;
+    request_rec r;
+    md_srv_conf_t *sc;
+    md_mod_conf_t *mc;
+    apr_status_t rv = APR_SUCCESS;
+    int i;
+    const char *domain;
+    apr_array_header_t *servers;
+    
+    sc = md_config_get(base_server);
+    mc = sc->mc;
+
+    /* Assign the MD to all server_rec configs that it matches. If there already
+     * is an assigned MD not equal this one, the configuration is in error.
+     */
+    memset(&r, 0, sizeof(r));
+    servers = apr_array_make(ptemp, 5, sizeof(server_rec*));
+    
+    for (s = base_server; s; s = s->next) {
+        r.server = s;
+        
+        for (i = 0; i < md->domains->nelts; ++i) {
+            domain = APR_ARRAY_IDX(md->domains, i, const char*);
+            
+            if (ap_matches_request_vhost(&r, domain, s->port)) {
+                /* Create a unique md_srv_conf_t record for this server, if there is none yet */
+                sc = md_config_get_unique(s, p);
+                
+                ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10041)
+                             "Server %s:%d matches md %s (config %s)", 
+                             s->server_hostname, s->port, md->name, sc->name);
+                
+                if (sc->assigned == md) {
+                    /* already matched via another domain name */
+                    goto next_server;
+                }
+                else if (sc->assigned) {
+                    ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10042)
+                                 "conflict: MD %s matches server %s, but MD %s also matches.",
+                                 md->name, s->server_hostname, sc->assigned->name);
+                    return APR_EINVAL;
+                }
+                
+                /* If this server_rec is only for http: requests. Defined
+                 * alias names to not matter for this MD.
+                 * (see gh issue https://github.com/icing/mod_md/issues/57)
+                 * Otherwise, if server has name or an alias not covered,
+                 * it is by default auto-added (config transitive).
+                 * If mode is "manual", a generated certificate will not match
+                 * all necessary names. */
+                if ((!mc->local_80 || !uses_port_only(s, mc->local_80))
+                    && APR_SUCCESS != (rv = md_covers_server(md, s, ptemp))) {
+                    return rv;
+                }
+
+                sc->assigned = md;
+                APR_ARRAY_PUSH(servers, server_rec*) = s;
+                
+                ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10043)
+                             "Managed Domain %s applies to vhost %s:%d", md->name,
+                             s->server_hostname, s->port);
+                
+                goto next_server;
+            }
+        }
+    next_server:
+        continue;
+    }
+
+    if (APR_SUCCESS == rv) {
+        if (apr_is_empty_array(servers)) {
+            if (md->drive_mode != MD_DRIVE_ALWAYS) {
+                /* Not an error, but looks suspicious */
+                ap_log_error(APLOG_MARK, APLOG_WARNING, 0, base_server, APLOGNO(10045)
+                             "No VirtualHost matches Managed Domain %s", md->name);
+                APR_ARRAY_PUSH(mc->unused_names, const char*)  = md->name;
+            }
+        }
+        else {
+            const char *uri;
+            
+            /* Found matching server_rec's. Collect all 'ServerAdmin's into MD's contact list */
+            apr_array_clear(md->contacts);
+            for (i = 0; i < servers->nelts; ++i) {
+                s = APR_ARRAY_IDX(servers, i, server_rec*);
+                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; 
+                        ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10044)
+                                     "%s: added contact %s", md->name, uri);
+                    }
+                }
+            }
+            
+            if (md->require_https > MD_REQUIRE_OFF) {
+                /* We require https for this MD, but do we have port 443 (or a mapped one)
+                 * available? */
+                if (mc->local_443 <= 0) {
+                    ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO()
+                                 "MDPortMap says there is no port for https (443), "
+                                 "but MD %s is configured to require https. This "
+                                 "only works when a 443 port is available.", md->name);
+                    return APR_EINVAL;
+                    
+                }
+                
+                /* Ok, we know which local port represents 443, do we have a server_rec
+                 * for MD that has addresses with port 443? */
+                s_https = NULL;
+                for (i = 0; i < servers->nelts; ++i) {
+                    s = APR_ARRAY_IDX(servers, i, server_rec*);
+                    if (matches_port_somewhere(s, mc->local_443)) {
+                        s_https = s;
+                        break;
+                    }
+                }
+                
+                if (!s_https) {
+                    /* Did not find any server_rec that matches this MD *and* has an
+                     * s->addrs match for the https port. Suspicious. */
+                    ap_log_error(APLOG_MARK, APLOG_WARNING, 0, base_server, APLOGNO()
+                                 "MD %s is configured to require https, but there seems to be "
+                                 "no VirtualHost for it that has port %d in its address list. "
+                                 "This looks as if it will not work.", 
+                                 md->name, mc->local_443);
+                }
+            }
+        }
+        
+    }
+    return rv;
+}
+
+static apr_status_t md_calc_md_list(apr_pool_t *p, apr_pool_t *plog,
+                                    apr_pool_t *ptemp, server_rec *base_server)
+{
+    md_srv_conf_t *sc;
+    md_mod_conf_t *mc;
+    md_t *md, *omd;
+    const char *domain;
+    apr_status_t rv = APR_SUCCESS;
+    ap_listen_rec *lr;
+    apr_sockaddr_t *sa;
+    int i, j;
+
+    (void)plog;
+    sc = md_config_get(base_server);
+    mc = sc->mc;
+    
+    mc->can_http = 0;
+    mc->can_https = 0;
+
+    for (lr = ap_listeners; lr; lr = lr->next) {
+        for (sa = lr->bind_addr; sa; sa = sa->next) {
+            if  (sa->port == mc->local_80 
+                 && (!lr->protocol || !strncmp("http", lr->protocol, 4))) {
+                mc->can_http = 1;
+            }
+            else if (sa->port == mc->local_443
+                     && (!lr->protocol || !strncmp("http", lr->protocol, 4))) {
+                mc->can_https = 1;
+            }
+        }
+    }
+    
+    ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10037)
+                 "server seems%s reachable via http: (port 80->%d) "
+                 "and%s reachable via https: (port 443->%d) ",
+                 mc->can_http? "" : " not", mc->local_80,
+                 mc->can_https? "" : " not", mc->local_443);
+    
+    /* Complete the properties of the MDs, now that we have the complete, merged
+     * server configurations. 
+     */
+    for (i = 0; i < mc->mds->nelts; ++i) {
+        md = APR_ARRAY_IDX(mc->mds, i, md_t*);
+        md_merge_srv(md, sc, p);
+
+        /* Check that we have no overlap with the MDs already completed */
+        for (j = 0; j < i; ++j) {
+            omd = APR_ARRAY_IDX(mc->mds, j, md_t*);
+            if ((domain = md_common_name(md, omd)) != NULL) {
+                ap_log_error(APLOG_MARK, APLOG_ERR, 0, base_server, APLOGNO(10038)
+                             "two Managed Domains have an overlap in domain '%s'"
+                             ", first definition in %s(line %d), second in %s(line %d)",
+                             domain, md->defn_name, md->defn_line_number,
+                             omd->defn_name, omd->defn_line_number);
+                return APR_EINVAL;
+            }
+        }
+
+        /* Assign MD to the server_rec configs that it matches. Perform some
+         * last finishing touches on the MD. */
+        if (APR_SUCCESS != (rv = assign_to_servers(md, base_server, p, ptemp))) {
+            return rv;
+        }
+
+        ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, base_server, APLOGNO(10039)
+                     "Completed MD[%s, CA=%s, Proto=%s, Agreement=%s, Drive=%d, renew=%ld]",
+                     md->name, md->ca_url, md->ca_proto, md->ca_agreement,
+                     md->drive_mode, (long)md->renew_window);
+    }
+    
+    return rv;
+}
+
+/**************************************************************************************************/
+/* store & registry setup */
+
+static apr_status_t store_file_ev(void *baton, struct md_store_t *store,
+                                    md_store_fs_ev_t ev, int group, 
+                                    const char *fname, apr_filetype_e ftype,  
+                                    apr_pool_t *p)
+{
+    server_rec *s = baton;
+    apr_status_t rv;
+    
+    (void)store;
+    ap_log_error(APLOG_MARK, APLOG_TRACE3, 0, s, "store event=%d on %s %s (group %d)", 
+                 ev, (ftype == APR_DIR)? "dir" : "file", fname, group);
+                 
+    /* Directories in group CHALLENGES and STAGING are written to by our watchdog,
+     * running on certain mpms in a child process under a different user. Give them
+     * ownership. 
+     */
+    if (ftype == APR_DIR) {
+        switch (group) {
+            case MD_SG_CHALLENGES:
+            case MD_SG_STAGING:
+                rv = md_make_worker_accessible(fname, p);
+                if (APR_ENOTIMPL != rv) {
+                    return rv;
+                }
+                break;
+            default: 
+                break;
+        }
+    }
+    return APR_SUCCESS;
+}
+
+static apr_status_t check_group_dir(md_store_t *store, md_store_group_t group, 
+                                    apr_pool_t *p, server_rec *s)
+{
+    const char *dir;
+    apr_status_t rv;
+    
+    if (APR_SUCCESS == (rv = md_store_get_fname(&dir, store, group, NULL, NULL, p))
+        && APR_SUCCESS == (rv = apr_dir_make_recursive(dir, MD_FPROT_D_UALL_GREAD, p))) {
+        rv = store_file_ev(s, store, MD_S_FS_EV_CREATED, group, dir, APR_DIR, p);
+    }
+    return rv;
+}
+
+static apr_status_t setup_store(md_store_t **pstore, md_mod_conf_t *mc, 
+                                apr_pool_t *p, server_rec *s)
+{
+    const char *base_dir;
+    apr_status_t rv;
+    
+    base_dir = ap_server_root_relative(p, mc->base_dir);
+    
+    if (APR_SUCCESS != (rv = md_store_fs_init(pstore, p, base_dir))) {
+        ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10046)"setup store for %s", base_dir);
+        goto out;
+    }
+
+    md_store_fs_set_event_cb(*pstore, store_file_ev, s);
+    if (APR_SUCCESS != (rv = check_group_dir(*pstore, MD_SG_CHALLENGES, p, s))) {
+        ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10047) 
+                     "setup challenges directory");
+        goto out;
+    }
+    if (APR_SUCCESS != (rv = check_group_dir(*pstore, MD_SG_STAGING, p, s))) {
+        ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10048) 
+                     "setup staging directory");
+        goto out;
+    }
+    if (APR_SUCCESS != (rv = check_group_dir(*pstore, MD_SG_ACCOUNTS, p, s))) {
+        ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10049) 
+                     "setup accounts directory");
+        goto out;
+    }
+    
+out:
+    return rv;
+}
+
+static apr_status_t setup_reg(md_reg_t **preg, apr_pool_t *p, server_rec *s, 
+                              int can_http, int can_https)
+{
+    md_srv_conf_t *sc;
+    md_mod_conf_t *mc;
+    md_store_t *store;
+    apr_status_t rv;
+    
+    sc = md_config_get(s);
+    mc = sc->mc;
+    
+    if (APR_SUCCESS == (rv = setup_store(&store, mc, p, s))
+        && APR_SUCCESS == (rv = md_reg_init(preg, p, store, mc->proxy_url))) {
+        mc->reg = *preg;
+        return md_reg_set_props(*preg, p, can_http, can_https); 
+    }
+    return rv;
+}
+
+/**************************************************************************************************/
+/* logging setup */
+
+static server_rec *log_server;
+
+static int log_is_level(void *baton, apr_pool_t *p, md_log_level_t level)
+{
+    (void)baton;
+    (void)p;
+    if (log_server) {
+        return APLOG_IS_LEVEL(log_server, (int)level);
+    }
+    return level <= MD_LOG_INFO;
+}
+
+#define LOG_BUF_LEN 16*1024
+
+static void log_print(const char *file, int line, md_log_level_t level, 
+                      apr_status_t rv, void *baton, apr_pool_t *p, const char *fmt, va_list ap)
+{
+    if (log_is_level(baton, p, level)) {
+        char buffer[LOG_BUF_LEN];
+        
+        memset(buffer, 0, sizeof(buffer));
+        apr_vsnprintf(buffer, LOG_BUF_LEN-1, fmt, ap);
+        buffer[LOG_BUF_LEN-1] = '\0';
+
+        if (log_server) {
+            ap_log_error(file, line, APLOG_MODULE_INDEX, level, rv, log_server, "%s",buffer);
+        }
+        else {
+            ap_log_perror(file, line, APLOG_MODULE_INDEX, level, rv, p, "%s", buffer);
+        }
+    }
+}
+
+/**************************************************************************************************/
+/* lifecycle */
+
+static apr_status_t cleanup_setups(void *dummy)
+{
+    (void)dummy;
+    log_server = NULL;
+    return APR_SUCCESS;
+}
+
+static void init_setups(apr_pool_t *p, server_rec *base_server) 
+{
+    log_server = base_server;
+    apr_pool_cleanup_register(p, NULL, cleanup_setups, apr_pool_cleanup_null);
+}
+
+/**************************************************************************************************/
+/* mod_ssl interface */
+
+static APR_OPTIONAL_FN_TYPE(ssl_is_https) *opt_ssl_is_https;
+
+static void init_ssl(void)
+{
+    opt_ssl_is_https = APR_RETRIEVE_OPTIONAL_FN(ssl_is_https);
+}
+
+/**************************************************************************************************/
+/* watchdog based impl. */
+
+#define MD_WATCHDOG_NAME   "_md_"
+
+static APR_OPTIONAL_FN_TYPE(ap_watchdog_get_instance) *wd_get_instance;
+static APR_OPTIONAL_FN_TYPE(ap_watchdog_register_callback) *wd_register_callback;
+static APR_OPTIONAL_FN_TYPE(ap_watchdog_set_callback_interval) *wd_set_interval;
+
+typedef struct {
+    md_t *md;
+
+    int stalled;
+    int renewed;
+    int renewal_notified;
+    apr_time_t restart_at;
+    int need_restart;
+    int restart_processed;
+
+    apr_status_t last_rv;
+    apr_time_t next_check;
+    int error_runs;
+} md_job_t;
+
+typedef struct {
+    apr_pool_t *p;
+    server_rec *s;
+    md_mod_conf_t *mc;
+    ap_watchdog_t *watchdog;
+    
+    apr_time_t next_change;
+    
+    apr_array_header_t *jobs;
+    md_reg_t *reg;
+} md_watchdog;
+
+static void assess_renewal(md_watchdog *wd, md_job_t *job, apr_pool_t *ptemp) 
+{
+    apr_time_t now = apr_time_now();
+    if (now >= job->restart_at) {
+        job->need_restart = 1;
+        ap_log_error( APLOG_MARK, APLOG_TRACE1, 0, wd->s, 
+                     "md(%s): has been renewed, needs restart now", job->md->name);
+    }
+    else {
+        job->next_check = job->restart_at;
+        
+        if (job->renewal_notified) {
+            ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, wd->s, 
+                         "%s: renewed cert valid in %s", 
+                         job->md->name, md_print_duration(ptemp, job->restart_at - now));
+        }
+        else {
+            char ts[APR_RFC822_DATE_LEN];
+
+            apr_rfc822_date(ts, job->restart_at);
+            ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, wd->s, APLOGNO(10051) 
+                         "%s: has been renewed successfully and should be activated at %s"
+                         " (this requires a server restart latest in %s)", 
+                         job->md->name, ts, md_print_duration(ptemp, job->restart_at - now));
+            job->renewal_notified = 1;
+        }
+    }
+}
+
+static apr_status_t load_job_props(md_reg_t *reg, md_job_t *job, apr_pool_t *p)
+{
+    md_store_t *store = md_reg_store_get(reg);
+    md_json_t *jprops;
+    apr_status_t rv;
+    
+    rv = md_store_load_json(store, MD_SG_STAGING, job->md->name,
+                            MD_FN_JOB, &jprops, p);
+    if (APR_SUCCESS == rv) {
+        job->restart_processed = md_json_getb(jprops, MD_KEY_PROCESSED, NULL);
+        job->error_runs = (int)md_json_getl(jprops, MD_KEY_ERRORS, NULL);
+    }
+    return rv;
+}
+
+static apr_status_t save_job_props(md_reg_t *reg, md_job_t *job, apr_pool_t *p)
+{
+    md_store_t *store = md_reg_store_get(reg);
+    md_json_t *jprops;
+    apr_status_t rv;
+    
+    rv = md_store_load_json(store, MD_SG_STAGING, job->md->name, MD_FN_JOB, &jprops, p);
+    if (APR_STATUS_IS_ENOENT(rv)) {
+        jprops = md_json_create(p);
+        rv = APR_SUCCESS;
+    }
+    if (APR_SUCCESS == rv) {
+        md_json_setb(job->restart_processed, jprops, MD_KEY_PROCESSED, NULL);
+        md_json_setl(job->error_runs, jprops, MD_KEY_ERRORS, NULL);
+        rv = md_store_save_json(store, p, MD_SG_STAGING, job->md->name,
+                                MD_FN_JOB, jprops, 0);
+    }
+    return rv;
+}
+
+static apr_status_t check_job(md_watchdog *wd, md_job_t *job, apr_pool_t *ptemp)
+{
+    apr_status_t rv = APR_SUCCESS;
+    apr_time_t valid_from, delay;
+    int errored, renew, error_runs;
+    char ts[APR_RFC822_DATE_LEN];
+    
+    if (apr_time_now() < job->next_check) {
+        /* Job needs to wait */
+        return APR_EAGAIN;
+    }
+    
+    job->next_check = 0;
+    error_runs = job->error_runs;
+
+    if (job->md->state == MD_S_MISSING) {
+        job->stalled = 1;
+    }
+    
+    if (job->stalled) {
+        /* Missing information, this will not change until configuration
+         * is changed and server restarted */
+        rv = APR_INCOMPLETE;
+        ++job->error_runs;
+        goto out;
+    }
+    else if (job->renewed) {
+        assess_renewal(wd, job, ptemp);
+    }
+    else if (APR_SUCCESS == (rv = md_reg_assess(wd->reg, job->md, &errored, &renew, wd->p))) {
+        if (errored) {
+            ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO(10050) 
+                         "md(%s): in error state", job->md->name);
+        }
+        else if (renew) {
+            ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO(10052) 
+                         "md(%s): state=%d, driving", job->md->name, job->md->state);
+                         
+            rv = md_reg_stage(wd->reg, job->md, NULL, 0, &valid_from, ptemp);
+            
+            if (APR_SUCCESS == rv) {
+                job->renewed = 1;
+                job->restart_at = valid_from;
+                assess_renewal(wd, job, ptemp);
+            }
+        }
+        else {
+            job->next_check = job->md->expires - job->md->renew_window;
+
+            apr_rfc822_date(ts, job->md->expires);
+            ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO(10053) 
+                         "md(%s): is complete, cert expires %s", job->md->name, ts);
+        }
+    }
+    
+    if (APR_SUCCESS == rv) {
+        job->error_runs = 0;
+    }
+    else {
+        ap_log_error( APLOG_MARK, APLOG_ERR, rv, wd->s, APLOGNO(10056) 
+                     "processing %s", job->md->name);
+        ++job->error_runs;
+        /* back off duration, depending on the errors we encounter in a row */
+        delay = apr_time_from_sec(5 << (job->error_runs - 1));
+        if (delay > apr_time_from_sec(60*60)) {
+            delay = apr_time_from_sec(60*60);
+        }
+        job->next_check = apr_time_now() + delay;
+        ap_log_error(APLOG_MARK, APLOG_INFO, 0, wd->s, APLOGNO(10057) 
+                     "%s: encountered error for the %d. time, next run in %s",
+                     job->md->name, job->error_runs, md_print_duration(ptemp, delay));
+    }
+    
+out:
+    if (error_runs != job->error_runs) {
+        apr_status_t rv2 = save_job_props(wd->reg, job, ptemp);
+        ap_log_error(APLOG_MARK, APLOG_TRACE1, rv2, wd->s, "%s: saving job props", job->md->name);
+    }
+
+    job->last_rv = rv;
+    return rv;
+}
+
+static apr_status_t run_watchdog(int state, void *baton, apr_pool_t *ptemp)
+{
+    md_watchdog *wd = baton;
+    apr_status_t rv = APR_SUCCESS;
+    md_job_t *job;
+    apr_time_t next_run, now;
+    int restart = 0;
+    int i;
+    
+    switch (state) {
+        case AP_WATCHDOG_STATE_STARTING:
+            ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO(10054)
+                         "md watchdog start, auto drive %d mds", wd->jobs->nelts);
+            assert(wd->reg);
+        
+            for (i = 0; i < wd->jobs->nelts; ++i) {
+                job = APR_ARRAY_IDX(wd->jobs, i, md_job_t *);
+                load_job_props(wd->reg, job, ptemp);
+            }
+            break;
+        case AP_WATCHDOG_STATE_RUNNING:
+        
+            wd->next_change = 0;
+            ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO(10055)
+                         "md watchdog run, auto drive %d mds", wd->jobs->nelts);
+                         
+            /* normally, we'd like to run at least twice a day */
+            next_run = apr_time_now() + apr_time_from_sec(MD_SECS_PER_DAY / 2);
+
+            /* Check on all the jobs we have */
+            for (i = 0; i < wd->jobs->nelts; ++i) {
+                job = APR_ARRAY_IDX(wd->jobs, i, md_job_t *);
+                
+                rv = check_job(wd, job, ptemp);
+
+                if (job->need_restart && !job->restart_processed) {
+                    restart = 1;
+                }
+                if (job->next_check && job->next_check < next_run) {
+                    next_run = job->next_check;
+                }
+            }
+
+            now = apr_time_now();
+            if (APLOGdebug(wd->s)) {
+                ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO()
+                             "next run in %s", md_print_duration(ptemp, next_run - now));
+            }
+            wd_set_interval(wd->watchdog, next_run - now, wd, run_watchdog);
+
+            for (i = 0; i < wd->jobs->nelts; ++i) {
+                job = APR_ARRAY_IDX(wd->jobs, i, md_job_t *);
+            }
+            break;
+            
+        case AP_WATCHDOG_STATE_STOPPING:
+            ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO(10058)
+                         "md watchdog stopping");
+            break;
+    }
+
+    if (restart) {
+        const char *action, *names = "";
+        int n;
+        
+        for (i = 0, n = 0; i < wd->jobs->nelts; ++i) {
+            job = APR_ARRAY_IDX(wd->jobs, i, md_job_t *);
+            if (job->need_restart && !job->restart_processed) {
+                names = apr_psprintf(ptemp, "%s%s%s", names, n? " " : "", job->md->name);
+                ++n;
+            }
+        }
+
+        if (n > 0) {
+            int notified = 1;
+
+            /* Run notify command for ready MDs (if configured) and persist that
+             * we have done so. This process might be reaped after n requests or die
+             * of another cause. The one taking over the watchdog need to notify again.
+             */
+            if (wd->mc->notify_cmd) {
+                const char * const *argv;
+                const char *cmdline;
+                int exit_code;
+                
+                cmdline = apr_psprintf(ptemp, "%s %s", wd->mc->notify_cmd, names); 
+                apr_tokenize_to_argv(cmdline, (char***)&argv, ptemp);
+                if (APR_SUCCESS == (rv = md_util_exec(ptemp, argv[0], argv, &exit_code))) {
+                    ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, wd->s, APLOGNO() 
+                                 "notify command '%s' returned %d", 
+                                 wd->mc->notify_cmd, exit_code);
+                }
+                else {
+                    ap_log_error(APLOG_MARK, APLOG_ERR, rv, wd->s, APLOGNO() 
+                                 "executing configured MDNotifyCmd %s", wd->mc->notify_cmd);
+                    notified = 0;
+                } 
+            }
+            
+            if (notified) {
+                /* persist the jobs that were notified */
+                for (i = 0, n = 0; i < wd->jobs->nelts; ++i) {
+                    job = APR_ARRAY_IDX(wd->jobs, i, md_job_t *);
+                    if (job->need_restart && !job->restart_processed) {
+                        job->restart_processed = 1;
+                        save_job_props(wd->reg, job, ptemp);
+                    }
+                }
+            }
+            
+            /* FIXME: the server needs to start gracefully to take the new certificate in.
+             * This poses a variety of problems to solve satisfactory for everyone:
+             * - I myself, have no implementation for Windows 
+             * - on *NIX, child processes run with less privileges, preventing
+             *   the signal based restart trigger to work
+             * - admins want better control of timing windows for restarts, e.g.
+             *   during less busy hours/days.
+             */
+            rv = md_server_graceful(ptemp, wd->s);
+            if (APR_ENOTIMPL == rv) {
+                /* self-graceful restart not supported in this setup */
+                action = " and changes will be activated on next (graceful) server restart.";
+            }
+            else {
+                action = " and server has been asked to restart now.";
+            }
+            ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, wd->s, APLOGNO(10059) 
+                         "The Managed Domain%s %s %s been setup%s",
+                         (n > 1)? "s" : "", names, (n > 1)? "have" : "has", action);
+        }
+    }
+    
+    return APR_SUCCESS;
+}
+
+static apr_status_t start_watchdog(apr_array_header_t *names, apr_pool_t *p, 
+                                   md_reg_t *reg, server_rec *s, md_mod_conf_t *mc)
+{
+    apr_allocator_t *allocator;
+    md_watchdog *wd;
+    apr_pool_t *wdp;
+    apr_status_t rv;
+    const char *name;
+    md_t *md;
+    md_job_t *job;
+    int i, errored, renew;
+    
+    wd_get_instance = APR_RETRIEVE_OPTIONAL_FN(ap_watchdog_get_instance);
+    wd_register_callback = APR_RETRIEVE_OPTIONAL_FN(ap_watchdog_register_callback);
+    wd_set_interval = APR_RETRIEVE_OPTIONAL_FN(ap_watchdog_set_callback_interval);
+    
+    if (!wd_get_instance || !wd_register_callback || !wd_set_interval) {
+        ap_log_error(APLOG_MARK, APLOG_CRIT, 0, s, APLOGNO(10061) "mod_watchdog is required");
+        return !OK;
+    }
+    
+    /* We want our own pool with own allocator to keep data across watchdog invocations */
+    apr_allocator_create(&allocator);
+    apr_allocator_max_free_set(allocator, ap_max_mem_free);
+    rv = apr_pool_create_ex(&wdp, p, NULL, allocator);
+    if (rv != APR_SUCCESS) {
+        ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10062) "md_watchdog: create pool");
+        return rv;
+    }
+    apr_allocator_owner_set(allocator, wdp);
+    apr_pool_tag(wdp, "md_watchdog");
+
+    wd = apr_pcalloc(wdp, sizeof(*wd));
+    wd->p = wdp;
+    wd->reg = reg;
+    wd->s = s;
+    wd->mc = mc;
+    
+    wd->jobs = apr_array_make(wd->p, 10, sizeof(md_job_t *));
+    for (i = 0; i < names->nelts; ++i) {
+        name = APR_ARRAY_IDX(names, i, const char *);
+        md = md_reg_get(wd->reg, name, wd->p);
+        if (md) {
+            md_reg_assess(wd->reg, md, &errored, &renew, wd->p);
+            if (errored) {
+                ap_log_error( APLOG_MARK, APLOG_WARNING, 0, wd->s, APLOGNO(10063) 
+                             "md(%s): seems errored. Will not process this any further.", name);
+            }
+            else {
+                job = apr_pcalloc(wd->p, sizeof(*job));
+                
+                job->md = md;
+                APR_ARRAY_PUSH(wd->jobs, md_job_t*) = job;
+
+                ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, wd->s, APLOGNO(10064) 
+                             "md(%s): state=%d, driving", name, md->state);
+                
+                load_job_props(reg, job, wd->p);
+                if (job->error_runs) {
+                    /* We are just restarting. If we encounter jobs that had errors
+                     * running the protocol on previous staging runs, we reset
+                     * the staging area for it, in case we persisted something that
+                     * causes a loop. */
+                    md_store_t *store = md_reg_store_get(wd->reg);
+                    
+                    md_store_purge(store, p, MD_SG_STAGING, job->md->name);
+                    md_store_purge(store, p, MD_SG_CHALLENGES, job->md->name);
+                }
+            }
+        }
+    }
+
+    if (!wd->jobs->nelts) {
+        ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10065)
+                     "no managed domain in state to drive, no watchdog needed, "
+                     "will check again on next server (graceful) restart");
+        apr_pool_destroy(wd->p);
+        return APR_SUCCESS;
+    }
+    
+    if (APR_SUCCESS != (rv = wd_get_instance(&wd->watchdog, MD_WATCHDOG_NAME, 0, 1, wd->p))) {
+        ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, APLOGNO(10066) 
+                     "create md watchdog(%s)", MD_WATCHDOG_NAME);
+        return rv;
+    }
+    rv = wd_register_callback(wd->watchdog, 0, wd, run_watchdog);
+    ap_log_error(APLOG_MARK, rv? APLOG_CRIT : APLOG_DEBUG, rv, s, APLOGNO(10067) 
+                 "register md watchdog(%s)", MD_WATCHDOG_NAME);
+    return rv;
+}
+static void load_stage_sets(apr_array_header_t *names, apr_pool_t *p, 
+                            md_reg_t *reg, server_rec *s)
+{
+    const char *name; 
+    apr_status_t rv;
+    int i;
+    
+    for (i = 0; i < names->nelts; ++i) {
+        name = APR_ARRAY_IDX(names, i, const char*);
+        if (APR_SUCCESS == (rv = md_reg_load(reg, name, p))) {
+            ap_log_error( APLOG_MARK, APLOG_INFO, rv, s, APLOGNO(10068) 
+                         "%s: staged set activated", name);
+        }
+        else if (!APR_STATUS_IS_ENOENT(rv)) {
+            ap_log_error( APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10069)
+                         "%s: error loading staged set", name);
+        }
+    }
+    return;
+}
+
+static apr_status_t md_check_config(apr_pool_t *p, apr_pool_t *plog,
+                                    apr_pool_t *ptemp, server_rec *s)
+{
+    const char *mod_md_init_key = "mod_md_init_counter";
+    void *data = NULL;
+    
+    apr_pool_userdata_get(&data, mod_md_init_key, s->process->pool);
+    if (data == NULL) {
+        ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10070)
+                     "initializing post config dry run");
+        apr_pool_userdata_set((const void *)1, mod_md_init_key,
+                              apr_pool_cleanup_null, s->process->pool);
+    }
+    
+    ap_log_error( APLOG_MARK, APLOG_INFO, 0, s, APLOGNO(10071)
+                 "mod_md (v%s), initializing...", MOD_MD_VERSION);
+
+    init_setups(p, s);
+    md_log_set(log_is_level, log_print, NULL);
+
+    /* Check uniqueness of MDs, calculate global, configured MD list.
+     * If successful, we have a list of MD definitions that do not overlap. */
+    /* We also need to find out if we can be reached on 80/443 from the outside (e.g. the CA) */
+    return md_calc_md_list(p, plog, ptemp, s);
+}
+    
+static apr_status_t md_post_config(apr_pool_t *p, apr_pool_t *plog,
+                                   apr_pool_t *ptemp, server_rec *s)
+{
+    md_srv_conf_t *sc;
+    md_mod_conf_t *mc;
+    md_reg_t *reg;
+    const md_t *md;
+    apr_array_header_t *drive_names;
+    apr_status_t rv = APR_SUCCESS;
+    int i;
+
+    (void)plog;
+    md_config_post_config(s, p);
+    sc = md_config_get(s);
+    mc = sc->mc;
+    
+    /* Synchronize the definitions we now have with the store via a registry (reg). */
+    if (APR_SUCCESS != (rv = setup_reg(&reg, p, s, mc->can_http, mc->can_https))) {
+        ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10072)
+                     "setup md registry");
+        goto out;
+    }
+    if (APR_SUCCESS != (rv = md_reg_sync(reg, p, ptemp, mc->mds))) {
+        ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO(10073)
+                     "synching %d mds to registry", mc->mds->nelts);
+    }
+    
+    /* Determine the managed domains that are in auto drive_mode. For those,
+     * determine in which state they are:
+     *  - UNKNOWN:            should not happen, report, dont drive
+     *  - ERROR:              something we do not know how to fix, report, dont drive
+     *  - INCOMPLETE/EXPIRED: need to drive them right away
+     *  - COMPLETE:           determine when cert expires, drive when the time comes
+     *
+     * Start the watchdog if we have anything, now or in the future.
+     */
+    drive_names = apr_array_make(ptemp, mc->mds->nelts+1, sizeof(const char *));
+    for (i = 0; i < mc->mds->nelts; ++i) {
+        md = APR_ARRAY_IDX(mc->mds, i, const md_t *);
+        switch (md->drive_mode) {
+            case MD_DRIVE_AUTO:
+                if (md_array_str_index(mc->unused_names, md->name, 0, 0) >= 0) {
+                    break;
+                }
+                /* fall through */
+            case MD_DRIVE_ALWAYS:
+                APR_ARRAY_PUSH(drive_names, const char *) = md->name; 
+                break;
+            default:
+                /* leave out */
+                break;
+        }
+    }
+    
+    init_ssl();
+    
+    /* If there are MDs to drive, start a watchdog to check on them regularly */
+    if (drive_names->nelts > 0) {
+        ap_log_error(APLOG_MARK, APLOG_DEBUG, rv, s, APLOGNO(10074)
+                     "%d out of %d mds are configured for auto-drive", 
+                     drive_names->nelts, mc->mds->nelts);
+    
+        load_stage_sets(drive_names, p, reg, s);
+        md_http_use_implementation(md_curl_get_impl(p));
+        rv = start_watchdog(drive_names, p, reg, s, mc);
+    }
+    else {
+        ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10075)
+                     "no mds to auto drive, no watchdog needed");
+    }
+out:
+    return rv;
+}
+
+/**************************************************************************************************/
+/* Access API to other httpd components */
+
+static int md_is_managed(server_rec *s)
+{
+    md_srv_conf_t *conf = md_config_get(s);
+
+    if (conf && conf->assigned) {
+        ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10076) 
+                     "%s: manages server %s", conf->assigned->name, s->server_hostname);
+        return 1;
+    }
+    ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s,  
+                 "server %s is not managed", s->server_hostname);
+    return 0;
+}
+
+static apr_status_t setup_fallback_cert(md_store_t *store, const md_t *md, apr_pool_t *p)
+{
+    md_pkey_t *pkey;
+    md_cert_t *cert;
+    md_pkey_spec_t spec;
+    apr_status_t rv;
+
+    spec.type = MD_PKEY_TYPE_RSA;
+    spec.params.rsa.bits = MD_PKEY_RSA_BITS_DEF;
+        
+    if (   APR_SUCCESS == (rv = md_pkey_gen(&pkey, p, &spec))
+        && APR_SUCCESS == (rv = md_store_save(store, p, MD_SG_DOMAINS, md->name, 
+                                              MD_FN_FALLBACK_PKEY, MD_SV_PKEY, (void*)pkey, 0))
+        && APR_SUCCESS == (rv = md_cert_self_sign(&cert, "Apache Managed Domain Fallback", 
+                                                  md->domains, pkey, 
+                                                  apr_time_from_sec(14 * MD_SECS_PER_DAY), p))) {
+        rv = md_store_save(store, p, MD_SG_DOMAINS, md->name, 
+                           MD_FN_FALLBACK_CERT, MD_SV_CERT, (void*)cert, 0);
+    }
+
+    return rv;
+}
+
+static int fexists(const char *fname, apr_pool_t *p)
+{
+    return (*fname && APR_SUCCESS == md_util_is_file(fname, p));
+}
+
+static apr_status_t md_get_certificate(server_rec *s, apr_pool_t *p,
+                                       const char **pkeyfile, const char **pcertfile)
+{
+    apr_status_t rv = APR_ENOENT;    
+    md_srv_conf_t *sc;
+    md_reg_t *reg;
+    md_store_t *store;
+    const md_t *md;
+    
+    *pkeyfile = NULL;
+    *pcertfile = NULL;
+    
+    sc = md_config_get(s);
+    
+    if (sc && sc->assigned) {
+        assert(sc->mc);
+        reg = sc->mc->reg;
+        assert(reg);
+        store = md_reg_store_get(reg);
+        assert(store);
+
+        md = md_reg_get(reg, sc->assigned->name, p);
+            
+        if (APR_SUCCESS != (rv = md_reg_get_cred_files(reg, md, p, pkeyfile, pcertfile))) {
+            ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, APLOGNO() 
+                         "retrieving credentials for MD %s", md->name);
+            return rv;
+        }
+
+        if (!fexists(*pkeyfile, p) || !fexists(*pcertfile, p)) { 
+            /* Provide temporary, self-signed certificate as fallback, so that
+             * clients do not get obscure TLS handshake errors or will see a fallback
+             * virtual host that is not intended to be served here. */
+             
+            md_store_get_fname(pkeyfile, store, MD_SG_DOMAINS, 
+                               md->name, MD_FN_FALLBACK_PKEY, p);
+            md_store_get_fname(pcertfile, store, MD_SG_DOMAINS, 
+                               md->name, MD_FN_FALLBACK_CERT, p);
+            if (!fexists(*pkeyfile, p) || !fexists(*pcertfile, p)) { 
+                if (APR_SUCCESS != (rv = setup_fallback_cert(store, md, p))) {
+                    ap_log_error(APLOG_MARK, APLOG_TRACE1, rv, s,  
+                                 "%s: setup fallback certificate", md->name);
+                    return rv;
+                }
+            }
+            
+            ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s,  
+                         "%s: providing fallback certificate for server %s", 
+                         md->name, s->server_hostname);
+            return APR_EAGAIN;
+        }
+
+        /* We have key and cert files, but they might no longer be valid or not
+         * match all domain names. Still use these files for now, but indicate that 
+         * resources should no longer be served until we have a new certificate again. */
+        if (md->state != MD_S_COMPLETE) {
+            return APR_EAGAIN;
+        }
+        ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10077) 
+                     "%s: providing certificate for server %s", md->name, s->server_hostname);
+    }
+    return rv;
+}
+
+static int md_is_challenge(conn_rec *c, const char *servername,
+                           X509 **pcert, EVP_PKEY **pkey)
+{
+    md_srv_conf_t *sc;
+    apr_size_t slen, sufflen = sizeof(MD_TLSSNI01_DNS_SUFFIX) - 1;
+    apr_status_t rv;
+
+    slen = strlen(servername);
+    if (slen <= sufflen 
+        || apr_strnatcasecmp(MD_TLSSNI01_DNS_SUFFIX, servername + slen - sufflen)) {
+        return 0;
+    }
+    
+    sc = md_config_get(c->base_server);
+    if (sc && sc->mc->reg) {
+        md_store_t *store = md_reg_store_get(sc->mc->reg);
+        md_cert_t *mdcert;
+        md_pkey_t *mdpkey;
+        
+        rv = md_store_load(store, MD_SG_CHALLENGES, servername, 
+                           MD_FN_TLSSNI01_CERT, MD_SV_CERT, (void**)&mdcert, c->pool);
+        if (APR_SUCCESS == rv && (*pcert = md_cert_get_X509(mdcert))) {
+            rv = md_store_load(store, MD_SG_CHALLENGES, servername, 
+                               MD_FN_TLSSNI01_PKEY, MD_SV_PKEY, (void**)&mdpkey, c->pool);
+            if (APR_SUCCESS == rv && (*pkey = md_pkey_get_EVP_PKEY(mdpkey))) {
+                ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, c, APLOGNO(10078)
+                              "%s: is a tls-sni-01 challenge host", servername);
+                return 1;
+            }
+            ap_log_cerror(APLOG_MARK, APLOG_WARNING, rv, c, APLOGNO(10079)
+                          "%s: challenge data not complete, key unavailable", servername);
+        }
+        else {
+            ap_log_cerror(APLOG_MARK, APLOG_INFO, rv, c, APLOGNO(10080)
+                          "%s: unknown TLS SNI challenge host", servername);
+        }
+    }
+    *pcert = NULL;
+    *pkey = NULL;
+    return 0;
+}
+
+/**************************************************************************************************/
+/* ACME challenge responses */
+
+#define WELL_KNOWN_PREFIX           "/.well-known/"
+#define ACME_CHALLENGE_PREFIX       WELL_KNOWN_PREFIX"acme-challenge/"
+
+static int md_http_challenge_pr(request_rec *r)
+{
+    apr_bucket_brigade *bb;
+    const md_srv_conf_t *sc;
+    const char *name, *data;
+    md_reg_t *reg;
+    apr_status_t rv;
+    
+    if (!strncmp(ACME_CHALLENGE_PREFIX, r->parsed_uri.path, sizeof(ACME_CHALLENGE_PREFIX)-1)) {
+        if (r->method_number == M_GET) {
+        
+            sc = ap_get_module_config(r->server->module_config, &md_module);
+            reg = sc && sc->mc? sc->mc->reg : NULL;
+            name = r->parsed_uri.path + sizeof(ACME_CHALLENGE_PREFIX)-1;
+
+            r->status = HTTP_NOT_FOUND;
+            if (!ap_strchr_c(name, '/') && reg) {
+                md_store_t *store = md_reg_store_get(reg);
+                ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, 
+                              "Challenge for %s (%s)", r->hostname, r->uri);
+
+                rv = md_store_load(store, MD_SG_CHALLENGES, r->hostname, 
+                                   MD_FN_HTTP01, MD_SV_TEXT, (void**)&data, r->pool);
+                if (APR_SUCCESS == rv) {
+                    apr_size_t len = strlen(data);
+                    
+                    r->status = HTTP_OK;
+                    apr_table_setn(r->headers_out, "Content-Length", apr_ltoa(r->pool, (long)len));
+                    
+                    bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
+                    apr_brigade_write(bb, NULL, NULL, data, len);
+                    ap_pass_brigade(r->output_filters, bb);
+                    apr_brigade_cleanup(bb);
+                }
+                else if (APR_STATUS_IS_ENOENT(rv)) {
+                    return HTTP_NOT_FOUND;
+                }
+                else if (APR_ENOENT != rv) {
+                    ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, APLOGNO(10081)
+                                  "loading challenge %s from store", name);
+                    return HTTP_INTERNAL_SERVER_ERROR;
+                }
+            }
+            return r->status;
+        }
+        else {
+            return HTTP_NOT_IMPLEMENTED;
+        }
+    }
+    return DECLINED;
+}
+
+/**************************************************************************************************/
+/* Require Https hook */
+
+static int md_require_https_maybe(request_rec *r)
+{
+    const md_srv_conf_t *sc;
+    apr_uri_t uri;
+    const char *s;
+    int status;
+    
+    if (opt_ssl_is_https 
+        && strncmp(WELL_KNOWN_PREFIX, r->parsed_uri.path, sizeof(WELL_KNOWN_PREFIX)-1)) {
+        
+        sc = ap_get_module_config(r->server->module_config, &md_module);
+        if (sc && sc->assigned && sc->assigned->require_https > MD_REQUIRE_OFF) {
+            if (opt_ssl_is_https(r->connection)) {
+                /* Using https:
+                 * if 'permanent' and no one else set a HSTS header already, do it */
+                if (sc->assigned->require_https == MD_REQUIRE_PERMANENT 
+                    && sc->mc->hsts_header && !apr_table_get(r->headers_out, MD_HSTS_HEADER)) {
+                    apr_table_setn(r->headers_out, MD_HSTS_HEADER, sc->mc->hsts_header);
+                }
+            }
+            else {
+                /* Not using https:, but require it. Redirect. */
+                if (r->method_number == M_GET) {
+                    /* safe to use the old-fashioned codes */
+                    status = ((MD_REQUIRE_PERMANENT == sc->assigned->require_https)? 
+                              HTTP_MOVED_PERMANENTLY : HTTP_MOVED_TEMPORARILY);
+                }
+                else {
+                    /* these should keep the method unchanged on retry */
+                    status = ((MD_REQUIRE_PERMANENT == sc->assigned->require_https)? 
+                              HTTP_PERMANENT_REDIRECT : HTTP_TEMPORARY_REDIRECT);
+                }
+                
+                s = ap_construct_url(r->pool, r->uri, r);
+                if (APR_SUCCESS == apr_uri_parse(r->pool, s, &uri)) {
+                    uri.scheme = (char*)"https";
+                    uri.port = 443;
+                    uri.port_str = (char*)"443";
+                    uri.query = r->parsed_uri.query;
+                    uri.fragment = r->parsed_uri.fragment;
+                    s = apr_uri_unparse(r->pool, &uri, APR_URI_UNP_OMITUSERINFO);
+                    if (s && *s) {
+                        apr_table_setn(r->headers_out, "Location", s);
+                        return status;
+                    }
+                }
+            }
+        }
+    }
+    return DECLINED;
+}
+
+/* Runs once per created child process. Perform any process 
+ * related initialization here.
+ */
+static void md_child_init(apr_pool_t *pool, server_rec *s)
+{
+    (void)pool;
+    (void)s;
+}
+
+/* Install this module into the apache2 infrastructure.
+ */
+static void md_hooks(apr_pool_t *pool)
+{
+    static const char *const mod_ssl[] = { "mod_ssl.c", NULL};
+
+    md_acme_init(pool, AP_SERVER_BASEVERSION);
+        
+    ap_log_perror(APLOG_MARK, APLOG_TRACE1, 0, pool, "installing hooks");
+    
+    /* Run once after configuration is set, before mod_ssl.
+     */
+    ap_hook_check_config(md_check_config, NULL, mod_ssl, APR_HOOK_MIDDLE);
+    ap_hook_post_config(md_post_config, NULL, mod_ssl, APR_HOOK_MIDDLE);
+    
+    /* Run once after a child process has been created.
+     */
+    ap_hook_child_init(md_child_init, NULL, mod_ssl, APR_HOOK_MIDDLE);
+
+    /* answer challenges *very* early, before any configured authentication may strike */
+    ap_hook_post_read_request(md_require_https_maybe, NULL, NULL, APR_HOOK_FIRST);
+    ap_hook_post_read_request(md_http_challenge_pr, NULL, NULL, APR_HOOK_MIDDLE);
+
+    APR_REGISTER_OPTIONAL_FN(md_is_managed);
+    APR_REGISTER_OPTIONAL_FN(md_get_certificate);
+    APR_REGISTER_OPTIONAL_FN(md_is_challenge);
+}
+
diff --git a/modules/md/mod_md.dsp b/modules/md/mod_md.dsp
new file mode 100644 (file)
index 0000000..edc7f85
--- /dev/null
@@ -0,0 +1,180 @@
+# Microsoft Developer Studio Project File - Name="mod_md" - Package Owner=<4>\r
+# Microsoft Developer Studio Generated Build File, Format Version 6.00\r
+# ** DO NOT EDIT **\r
+\r
+# TARGTYPE "Win32 (x86) Dynamic-Link Library" 0x0102\r
+\r
+CFG=mod_md - Win32 Release\r
+!MESSAGE This is not a valid makefile. To build this project using NMAKE,\r
+!MESSAGE use the Export Makefile command and run\r
+!MESSAGE \r
+!MESSAGE NMAKE /f "mod_md.mak".\r
+!MESSAGE \r
+!MESSAGE You can specify a configuration when running NMAKE\r
+!MESSAGE by defining the macro CFG on the command line. For example:\r
+!MESSAGE \r
+!MESSAGE NMAKE /f "mod_md.mak" CFG="mod_md - Win32 Release"\r
+!MESSAGE \r
+!MESSAGE Possible choices for configuration are:\r
+!MESSAGE \r
+!MESSAGE "mod_md - Win32 Release" (based on "Win32 (x86) Dynamic-Link Library")\r
+!MESSAGE "mod_md - Win32 Debug" (based on "Win32 (x86) Dynamic-Link Library")\r
+!MESSAGE \r
+\r
+# Begin Project\r
+# PROP AllowPerConfigDependencies 0\r
+# PROP Scc_ProjName ""\r
+# PROP Scc_LocalPath ""\r
+CPP=cl.exe\r
+MTL=midl.exe\r
+RSC=rc.exe\r
+\r
+!IF  "$(CFG)" == "mod_md - Win32 Release"\r
+\r
+# PROP BASE Use_MFC 0\r
+# PROP BASE Use_Debug_Libraries 0\r
+# PROP BASE Output_Dir "Release"\r
+# PROP BASE Intermediate_Dir "Release"\r
+# PROP BASE Target_Dir ""\r
+# PROP Use_MFC 0\r
+# PROP Use_Debug_Libraries 0\r
+# PROP Output_Dir "Release"\r
+# PROP Intermediate_Dir "Release"\r
+# PROP Ignore_Export_Lib 0\r
+# PROP Target_Dir ""\r
+# ADD BASE CPP /nologo /MD /W3 /O2 /D "WIN32" /D "NDEBUG" /D "_WINDOWS" /D "ssize_t=long" /FD /c\r
+# ADD CPP /nologo /MD /W3 /O2 /Oy- /Zi /I "../../server/mpm/winnt" "/I ../ssl" /I "../../include" /I "../../srclib/apr/include" /I "../../srclib/apr-util/include" /I "../../srclib/openssl/inc32" /I "../../srclib/jansson/include" /I "../../srclib/curl/include" /I "../core"   /D "NDEBUG" /D "WIN32" /D "_WINDOWS" /D "ssize_t=long" /Fd"Release\mod_md_src" /FD /c\r
+# ADD BASE MTL /nologo /D "NDEBUG" /win32\r
+# ADD MTL /nologo /D "NDEBUG" /mktyplib203 /win32\r
+# ADD BASE RSC /l 0x409 /d "NDEBUG"\r
+# ADD RSC /l 0x409 /fo"Release/mod_md.res" /i "../../include" /i "../../srclib/apr/include" /d "NDEBUG" /d "BIN_NAME=mod_md.so" /d "LONG_NAME=Letsencrypt module for Apache"\r
+BSC32=bscmake.exe\r
+# ADD BASE BSC32 /nologo\r
+# ADD BSC32 /nologo\r
+LINK32=link.exe\r
+# ADD BASE LINK32 kernel32.lib /nologo /subsystem:windows /dll /out:".\Release\mod_md.so" /base:@..\..\os\win32\BaseAddr.ref,mod_md.so\r
+# ADD LINK32 kernel32.lib libhttpd.lib libapr-1.lib libaprutil-1.lib ssleay32.lib libeay32.lib jansson.lib libcurl.lib /libpath:"../../srclib/apr/Release" /libpath:"../../srclib/apr-util/Release" /libpath:"../../Release/" /libpath:"../../srclib/openssl/out32dll" /libpath:"../../srclib/jansson/lib" /libpath:"../../srclib/curl/lib" /nologo /subsystem:windows /dll /incremental:no /debug /out:".\Release\mod_md.so" /base:@..\..\os\win32\BaseAddr.ref,mod_md.so /opt:ref\r
+# Begin Special Build Tool\r
+TargetPath=.\Release\mod_md.so\r
+SOURCE="$(InputPath)"\r
+PostBuild_Desc=Embed .manifest\r
+PostBuild_Cmds=if exist $(TargetPath).manifest mt.exe -manifest $(TargetPath).manifest -outputresource:$(TargetPath);2\r
+# End Special Build Tool\r
+\r
+!ELSEIF  "$(CFG)" == "mod_md - Win32 Debug"\r
+\r
+# PROP BASE Use_MFC 0\r
+# PROP BASE Use_Debug_Libraries 1\r
+# PROP BASE Output_Dir "Debug"\r
+# PROP BASE Intermediate_Dir "Debug"\r
+# PROP BASE Target_Dir ""\r
+# PROP Use_MFC 0\r
+# PROP Use_Debug_Libraries 1\r
+# PROP Output_Dir "Debug"\r
+# PROP Intermediate_Dir "Debug"\r
+# PROP Ignore_Export_Lib 0\r
+# PROP Target_Dir ""\r
+# ADD BASE CPP /nologo /MDd /W3 /EHsc /Zi /Od /D "WIN32" /D "_DEBUG" /D "_WINDOWS" /D "ssize_t=long" /FD /c\r
+# ADD CPP /nologo /MDd /W3 /EHsc /Zi /Od /I "../ssl" /I "../../include" /I "../../srclib/apr/include" /I "../../srclib/apr-util/include" /I "../../srclib/openssl/inc32" /I "../../srclib/jansson/include" /I "../../srclib/curl/include" /I "../core" /src" /D "_DEBUG" /D "WIN32" /D "_WINDOWS" /D "ssize_t=long" /Fd"Debug\mod_md_src" /FD /c\r
+# ADD BASE MTL /nologo /D "_DEBUG" /win32\r
+# ADD MTL /nologo /D "_DEBUG" /mktyplib203 /win32\r
+# ADD BASE RSC /l 0x409 /d "_DEBUG"\r
+# ADD RSC /l 0x409 /fo"Debug/mod_md.res" /i "../../include" /i "../../srclib/apr/include" /d "_DEBUG" /d "BIN_NAME=mod_md.so" /d "LONG_NAME=md_module for Apache"\r
+BSC32=bscmake.exe\r
+# ADD BASE BSC32 /nologo\r
+# ADD BSC32 /nologo\r
+LINK32=link.exe\r
+# ADD BASE LINK32 kernel32.lib /nologo /subsystem:windows /dll /incremental:no /debug /out:".\Debug\mod_md.so" /base:@..\..\os\win32\BaseAddr.ref,mod_md.so\r
+# ADD LINK32 kernel32.lib libhttpd.lib libapr-1.lib libaprutil-1.lib ssleay32.lib libeay32.lib jansson_d.lib libcurl.lib /nologo /subsystem:windows /dll /libpath:"../../srclib/openssl/out32dll" /libpath:"../../srclib/jansson/lib" /libpath:"../../srclib/curl/lib" /incremental:no /debug /out:".\Debug\mod_md.so" /base:@..\..\os\win32\BaseAddr.ref,mod_md.so\r
+# Begin Special Build Tool\r
+TargetPath=.\Debug\mod_md.so\r
+SOURCE="$(InputPath)"\r
+PostBuild_Desc=Embed .manifest\r
+PostBuild_Cmds=if exist $(TargetPath).manifest mt.exe -manifest $(TargetPath).manifest -outputresource:$(TargetPath);2\r
+# End Special Build Tool\r
+\r
+!ENDIF \r
+\r
+# Begin Target\r
+\r
+# Name "mod_md - Win32 Release"\r
+# Name "mod_md - Win32 Debug"\r
+# Begin Source File\r
+\r
+SOURCE=./mod_md.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+SOURCE=./mod_md_config.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+SOURCE=./mod_md_os.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+SOURCE=./md_core.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+SOURCE=./md_crypt.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+SOURCE=./md_curl.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+SOURCE=./md_http.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+SOURCE=./md_json.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+SOURCE=./md_jws.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+SOURCE=./md_log.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+SOURCE=./md_reg.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+SOURCE=./md_store.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+SOURCE=./md_store_fs.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+SOURCE=./md_util.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+SOURCE=./md_acme.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+SOURCE=./md_acme_acct.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+SOURCE=./md_acme_authz.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+SOURCE=./md_acme_drive.c\r
+# End Source File\r
+# Begin Source File\r
+\r
+\r
+SOURCE=..\..\build\win32\httpd.rc\r
+# End Source File\r
+# End Target\r
+# End Project\r
diff --git a/modules/md/mod_md.h b/modules/md/mod_md.h
new file mode 100644 (file)
index 0000000..34edba8
--- /dev/null
@@ -0,0 +1,42 @@
+/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de)
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+#ifndef mod_md_mod_md_h
+#define mod_md_mod_md_h
+
+#include <openssl/evp.h>
+#include <openssl/x509v3.h>
+
+struct server_rec;
+
+APR_DECLARE_OPTIONAL_FN(int, 
+                        md_is_managed, (struct server_rec *));
+
+/**
+ * Get the certificate/key for the managed domain (md_is_managed != 0).
+ * 
+ * @return APR_EAGAIN if the real certificate is not available yet
+ */
+APR_DECLARE_OPTIONAL_FN(apr_status_t, 
+                        md_get_certificate, (struct server_rec *, apr_pool_t *,
+                                             const char **pkeyfile, 
+                                             const char **pcertfile));
+
+APR_DECLARE_OPTIONAL_FN(int, 
+                        md_is_challenge, (struct conn_rec *, const char *,
+                                          X509 **pcert, EVP_PKEY **pkey));
+
+
+#endif /* mod_md_mod_md_h */
diff --git a/modules/md/mod_md_config.c b/modules/md/mod_md_config.c
new file mode 100644 (file)
index 0000000..4eff7af
--- /dev/null
@@ -0,0 +1,901 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+
+#include <apr_lib.h>
+#include <apr_strings.h>
+
+#include <httpd.h>
+#include <http_core.h>
+#include <http_config.h>
+#include <http_log.h>
+#include <http_vhost.h>
+
+#include "md.h"
+#include "md_crypt.h"
+#include "md_util.h"
+#include "mod_md_private.h"
+#include "mod_md_config.h"
+
+#define MD_CMD_MD             "ManagedDomain"
+#define MD_CMD_MD_SECTION     "<ManagedDomain"
+#define MD_CMD_CA             "MDCertificateAuthority"
+#define MD_CMD_CAAGREEMENT    "MDCertificateAgreement"
+#define MD_CMD_CACHALLENGES   "MDCAChallenges"
+#define MD_CMD_CAPROTO        "MDCertificateProtocol"
+#define MD_CMD_DRIVEMODE      "MDDriveMode"
+#define MD_CMD_MEMBER         "MDMember"
+#define MD_CMD_MEMBERS        "MDMembers"
+#define MD_CMD_MUSTSTAPLE     "MDMustStaple"
+#define MD_CMD_PORTMAP        "MDPortMap"
+#define MD_CMD_PKEYS          "MDPrivateKeys"
+#define MD_CMD_PROXY          "MDHttpProxy"
+#define MD_CMD_RENEWWINDOW    "MDRenewWindow"
+#define MD_CMD_REQUIREHTTPS   "MDRequireHttps"
+#define MD_CMD_STOREDIR       "MDStoreDir"
+#define MD_CMD_NOTIFYCMD      "MDNotifyCmd"
+
+#define DEF_VAL     (-1)
+
+/* Default settings for the global conf */
+static md_mod_conf_t defmc = {
+    NULL,
+    "md",
+    NULL,
+    NULL,
+    80,
+    443,
+    0,
+    0,
+    MD_HSTS_MAX_AGE_DEFAULT,
+    NULL,
+    NULL,
+    NULL,
+};
+
+/* Default server specific setting */
+static md_srv_conf_t defconf = {
+    "default",
+    NULL,
+    &defmc,
+
+    1,
+    MD_REQUIRE_OFF,
+    MD_DRIVE_AUTO,
+    0,
+    NULL, 
+    apr_time_from_sec(90 * MD_SECS_PER_DAY), /* If the cert lifetime were 90 days, renew */
+    apr_time_from_sec(30 * MD_SECS_PER_DAY), /* 30 days before. Adjust to actual lifetime */
+    MD_ACME_DEF_URL,
+    "ACME",
+    NULL,
+    NULL,
+    NULL,
+    NULL,
+};
+
+static md_mod_conf_t *mod_md_config;
+
+static apr_status_t cleanup_mod_config(void *dummy)
+{
+    (void)dummy;
+    mod_md_config = NULL;
+    return APR_SUCCESS;
+}
+
+static md_mod_conf_t *md_mod_conf_get(apr_pool_t *pool, int create)
+{
+    if (mod_md_config) {
+        return mod_md_config; /* reused for lifetime of the pool */
+    }
+
+    if (create) {
+        mod_md_config = apr_pcalloc(pool, sizeof(*mod_md_config));
+        memcpy(mod_md_config, &defmc, sizeof(*mod_md_config));
+        mod_md_config->mds = apr_array_make(pool, 5, sizeof(const md_t *));
+        mod_md_config->unused_names = apr_array_make(pool, 5, sizeof(const md_t *));
+        
+        apr_pool_cleanup_register(pool, NULL, cleanup_mod_config, apr_pool_cleanup_null);
+    }
+    
+    return mod_md_config;
+}
+
+#define CONF_S_NAME(s)  (s && s->server_hostname? s->server_hostname : "default")
+
+static void srv_conf_props_clear(md_srv_conf_t *sc)
+{
+    sc->transitive = DEF_VAL;
+    sc->require_https = MD_REQUIRE_UNSET;
+    sc->drive_mode = DEF_VAL;
+    sc->must_staple = DEF_VAL;
+    sc->pkey_spec = NULL;
+    sc->renew_norm = DEF_VAL;
+    sc->renew_window = DEF_VAL;
+    sc->ca_url = NULL;
+    sc->ca_proto = NULL;
+    sc->ca_agreement = NULL;
+    sc->ca_challenges = NULL;
+}
+
+static void srv_conf_props_copy(md_srv_conf_t *to, const md_srv_conf_t *from)
+{
+    to->transitive = from->transitive;
+    to->require_https = from->require_https;
+    to->drive_mode = from->drive_mode;
+    to->must_staple = from->must_staple;
+    to->pkey_spec = from->pkey_spec;
+    to->renew_norm = from->renew_norm;
+    to->renew_window = from->renew_window;
+    to->ca_url = from->ca_url;
+    to->ca_proto = from->ca_proto;
+    to->ca_agreement = from->ca_agreement;
+    to->ca_challenges = from->ca_challenges;
+}
+
+static void srv_conf_props_apply(md_t *md, const md_srv_conf_t *from, apr_pool_t *p)
+{
+    if (from->require_https != MD_REQUIRE_UNSET) md->require_https = from->require_https;
+    if (from->transitive != DEF_VAL) md->transitive = from->transitive;
+    if (from->drive_mode != DEF_VAL) md->drive_mode = from->drive_mode;
+    if (from->must_staple != DEF_VAL) md->must_staple = from->must_staple;
+    if (from->pkey_spec) md->pkey_spec = from->pkey_spec;
+    if (from->renew_norm != DEF_VAL) md->renew_norm = from->renew_norm;
+    if (from->renew_window != DEF_VAL) md->renew_window = from->renew_window;
+
+    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_challenges) md->ca_challenges = apr_array_copy(p, from->ca_challenges);
+}
+
+void *md_config_create_svr(apr_pool_t *pool, server_rec *s)
+{
+    md_srv_conf_t *conf = (md_srv_conf_t *)apr_pcalloc(pool, sizeof(md_srv_conf_t));
+
+    conf->name = apr_pstrcat(pool, "srv[", CONF_S_NAME(s), "]", NULL);
+    conf->s = s;
+    conf->mc = md_mod_conf_get(pool, 1);
+
+    srv_conf_props_clear(conf);
+    
+    return conf;
+}
+
+static void *md_config_merge(apr_pool_t *pool, void *basev, void *addv)
+{
+    md_srv_conf_t *base = (md_srv_conf_t *)basev;
+    md_srv_conf_t *add = (md_srv_conf_t *)addv;
+    md_srv_conf_t *nsc;
+    char *name = apr_pstrcat(pool, "[", CONF_S_NAME(add->s), ", ", CONF_S_NAME(base->s), "]", NULL);
+    
+    nsc = (md_srv_conf_t *)apr_pcalloc(pool, sizeof(md_srv_conf_t));
+    nsc->name = name;
+    nsc->mc = add->mc? add->mc : base->mc;
+    nsc->assigned = add->assigned? add->assigned : base->assigned;
+
+    nsc->transitive = (add->transitive != DEF_VAL)? add->transitive : base->transitive;
+    nsc->require_https = (add->require_https != MD_REQUIRE_UNSET)? add->require_https : base->require_https;
+    nsc->drive_mode = (add->drive_mode != DEF_VAL)? add->drive_mode : base->drive_mode;
+    nsc->must_staple = (add->must_staple != DEF_VAL)? add->must_staple : base->must_staple;
+    nsc->pkey_spec = add->pkey_spec? add->pkey_spec : base->pkey_spec;
+    nsc->renew_window = (add->renew_norm != DEF_VAL)? add->renew_norm : base->renew_norm;
+    nsc->renew_window = (add->renew_window != DEF_VAL)? add->renew_window : base->renew_window;
+
+    nsc->ca_url = add->ca_url? add->ca_url : base->ca_url;
+    nsc->ca_proto = add->ca_proto? add->ca_proto : base->ca_proto;
+    nsc->ca_agreement = add->ca_agreement? add->ca_agreement : base->ca_agreement;
+    nsc->ca_challenges = (add->ca_challenges? apr_array_copy(pool, add->ca_challenges) 
+                    : (base->ca_challenges? apr_array_copy(pool, base->ca_challenges) : NULL));
+    nsc->current = NULL;
+    nsc->assigned = NULL;
+    
+    return nsc;
+}
+
+void *md_config_merge_svr(apr_pool_t *pool, void *basev, void *addv)
+{
+    return md_config_merge(pool, basev, addv);
+}
+
+static int inside_section(cmd_parms *cmd, const char *section) {
+    ap_directive_t *d;
+    for (d = cmd->directive->parent; d; d = d->parent) {
+       if (!ap_cstr_casecmp(d->directive, section)) {
+           return 1;
+       }
+    }
+    return 0; 
+}
+
+static const char *md_section_check(cmd_parms *cmd, const char *section) {
+    if (!inside_section(cmd, section)) {
+        return apr_pstrcat(cmd->pool, cmd->cmd->name, " is only valid inside a '", 
+                           section, "' context, not here", NULL);
+    }
+    return NULL;
+}
+
+static void add_domain_name(apr_array_header_t *domains, const char *name, apr_pool_t *p)
+{
+    if (md_array_str_index(domains, name, 0, 0) < 0) {
+        APR_ARRAY_PUSH(domains, char *) = md_util_str_tolower(apr_pstrdup(p, name));
+    }
+}
+
+static const char *set_transitive(int *ptransitive, const char *value)
+{
+    if (!apr_strnatcasecmp("auto", value)) {
+        *ptransitive = 1;
+        return NULL;
+    }
+    else if (!apr_strnatcasecmp("manual", value)) {
+        *ptransitive = 0;
+        return NULL;
+    }
+    return "unknown value, use \"auto|manual\"";
+}
+
+static const char *md_config_sec_start(cmd_parms *cmd, void *mconfig, const char *arg)
+{
+    md_srv_conf_t *sc;
+    md_srv_conf_t save;
+    const char *endp;
+    const char *err, *name;
+    apr_array_header_t *domains;
+    md_t *md;
+    int transitive = -1;
+    
+    (void)mconfig;
+    if ((err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) {
+        return err;
+    }
+        
+    sc = md_config_get(cmd->server);
+    endp = ap_strrchr_c(arg, '>');
+    if (endp == NULL) {
+        return  MD_CMD_MD_SECTION "> directive missing closing '>'";
+    }
+
+    arg = apr_pstrndup(cmd->pool, arg, (apr_size_t)(endp-arg));
+    if (!arg || !*arg) {
+        return MD_CMD_MD_SECTION " > section must specify a unique domain name";
+    }
+
+    name = ap_getword_white(cmd->pool, &arg);
+    domains = apr_array_make(cmd->pool, 5, sizeof(const char *));
+    add_domain_name(domains, name, cmd->pool);
+    while (*arg != '\0') {
+        name = ap_getword_white(cmd->pool, &arg);
+        if (NULL != set_transitive(&transitive, name)) {
+            add_domain_name(domains, name, cmd->pool);
+        }
+    }
+
+    if (domains->nelts == 0) {
+        return "needs at least one domain name";
+    }
+    
+    md = md_create(cmd->pool, domains);
+    if (transitive >= 0) {
+        md->transitive = transitive;
+    }
+    
+    /* Save the current settings in this srv_conf and apply+restore at the
+     * end of this section */
+    memcpy(&save, sc, sizeof(save));
+    srv_conf_props_clear(sc);
+    sc->current = md;
+    
+    if (NULL == (err = ap_walk_config(cmd->directive->first_child, cmd, cmd->context))) {
+        srv_conf_props_apply(md, sc, cmd->pool);
+        APR_ARRAY_PUSH(sc->mc->mds, const md_t *) = md;
+    }
+    
+    sc->current = NULL;
+    srv_conf_props_copy(sc, &save);
+    
+    return err;
+}
+
+static const char *md_config_sec_add_members(cmd_parms *cmd, void *dc, 
+                                             int argc, char *const argv[])
+{
+    md_srv_conf_t *sc = md_config_get(cmd->server);
+    const char *err;
+    int i;
+    
+    (void)dc;
+    if (NULL != (err = md_section_check(cmd, MD_CMD_MD_SECTION))) {
+        if (argc == 1) {
+            /* only these values are allowed outside a section */
+            return set_transitive(&sc->transitive, argv[0]);
+        }
+        return err;
+    }
+    
+    assert(sc->current);
+    for (i = 0; i < argc; ++i) {
+        if (NULL != set_transitive(&sc->transitive, argv[i])) {
+            add_domain_name(sc->current->domains, argv[i], cmd->pool);
+        }
+    }
+    return NULL;
+}
+
+static const char *md_config_set_names(cmd_parms *cmd, void *dc, 
+                                       int argc, char *const argv[])
+{
+    md_srv_conf_t *sc = md_config_get(cmd->server);
+    apr_array_header_t *domains = apr_array_make(cmd->pool, 5, sizeof(const char *));
+    const char *err;
+    md_t *md;
+    int i, transitive = -1;
+
+    (void)dc;
+    err = ap_check_cmd_context(cmd, NOT_IN_DIR_LOC_FILE);
+    if (err) {
+        return err;
+    }
+
+    for (i = 0; i < argc; ++i) {
+        if (NULL != set_transitive(&transitive, argv[i])) {
+            add_domain_name(domains, argv[i], cmd->pool);
+        }
+    }
+    
+    if (domains->nelts == 0) {
+        return "needs at least one domain name";
+    }
+    md = md_create(cmd->pool, domains);
+
+    if (transitive >= 0) {
+        md->transitive = transitive;
+    }
+    
+    if (cmd->config_file) {
+        md->defn_name = cmd->config_file->name;
+        md->defn_line_number = cmd->config_file->line_number;
+    }
+
+    APR_ARRAY_PUSH(sc->mc->mds, md_t *) = md;
+
+    return NULL;
+}
+
+static const char *md_config_set_ca(cmd_parms *cmd, void *dc, const char *value)
+{
+    md_srv_conf_t *sc = md_config_get(cmd->server);
+    const char *err;
+
+    (void)dc;
+    if (!inside_section(cmd, MD_CMD_MD_SECTION)
+        && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) {
+        return err;
+    }
+    sc->ca_url = value;
+    return NULL;
+}
+
+static const char *md_config_set_ca_proto(cmd_parms *cmd, void *dc, const char *value)
+{
+    md_srv_conf_t *config = md_config_get(cmd->server);
+    const char *err;
+
+    (void)dc;
+    if (!inside_section(cmd, MD_CMD_MD_SECTION)
+        && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) {
+        return err;
+    }
+    config->ca_proto = value;
+    return NULL;
+}
+
+static const char *md_config_set_agreement(cmd_parms *cmd, void *dc, const char *value)
+{
+    md_srv_conf_t *config = md_config_get(cmd->server);
+    const char *err;
+
+    (void)dc;
+    if (!inside_section(cmd, MD_CMD_MD_SECTION)
+        && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) {
+        return err;
+    }
+    config->ca_agreement = value;
+    return NULL;
+}
+
+static const char *md_config_set_drive_mode(cmd_parms *cmd, void *dc, const char *value)
+{
+    md_srv_conf_t *config = md_config_get(cmd->server);
+    const char *err;
+    md_drive_mode_t drive_mode;
+
+    (void)dc;
+    if (!apr_strnatcasecmp("auto", value) || !apr_strnatcasecmp("automatic", value)) {
+        drive_mode = MD_DRIVE_AUTO;
+    }
+    else if (!apr_strnatcasecmp("always", value)) {
+        drive_mode = MD_DRIVE_ALWAYS;
+    }
+    else if (!apr_strnatcasecmp("manual", value) || !apr_strnatcasecmp("stick", value)) {
+        drive_mode = MD_DRIVE_MANUAL;
+    }
+    else {
+        return apr_pstrcat(cmd->pool, "unknown MDDriveMode ", value, NULL);
+    }
+    
+    if (!inside_section(cmd, MD_CMD_MD_SECTION)
+        && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) {
+        return err;
+    }
+    config->drive_mode = drive_mode;
+    return NULL;
+}
+
+static const char *md_config_set_must_staple(cmd_parms *cmd, void *dc, const char *value)
+{
+    md_srv_conf_t *config = md_config_get(cmd->server);
+    const char *err;
+
+    (void)dc;
+    if (!inside_section(cmd, MD_CMD_MD_SECTION)
+        && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) {
+        return err;
+    }
+
+    if (!apr_strnatcasecmp("off", value)) {
+        config->must_staple = 0;
+    }
+    else if (!apr_strnatcasecmp("on", value)) {
+        config->must_staple = 1;
+    }
+    else {
+        return apr_pstrcat(cmd->pool, "unknown '", value, 
+                           "', supported parameter values are 'on' and 'off'", NULL);
+    }
+    return NULL;
+}
+
+static const char *md_config_set_require_https(cmd_parms *cmd, void *dc, const char *value)
+{
+    md_srv_conf_t *config = md_config_get(cmd->server);
+    const char *err;
+
+    (void)dc;
+    if (!inside_section(cmd, MD_CMD_MD_SECTION)
+        && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) {
+        return err;
+    }
+
+    if (!apr_strnatcasecmp("off", value)) {
+        config->require_https = MD_REQUIRE_OFF;
+    }
+    else if (!apr_strnatcasecmp(MD_KEY_TEMPORARY, value)) {
+        config->require_https = MD_REQUIRE_TEMPORARY;
+    }
+    else if (!apr_strnatcasecmp(MD_KEY_PERMANENT, value)) {
+        config->require_https = MD_REQUIRE_PERMANENT;
+    }
+    else {
+        return apr_pstrcat(cmd->pool, "unknown '", value, 
+                           "', supported parameter values are 'temporary' and 'permanent'", NULL);
+    }
+    return NULL;
+}
+
+static apr_status_t duration_parse(const char *value, apr_interval_time_t *ptimeout, 
+                                   const char *def_unit)
+{
+    char *endp;
+    long funits = 1;
+    apr_status_t rv;
+    apr_int64_t n;
+    
+    n = apr_strtoi64(value, &endp, 10);
+    if (errno) {
+        return errno;
+    }
+    if (!endp || !*endp) {
+        if (strcmp(def_unit, "d") == 0) {
+            def_unit = "s";
+            funits = MD_SECS_PER_DAY;
+        }
+    }
+    else if (endp == value) {
+        return APR_EINVAL;
+    }
+    else if (*endp == 'd') {
+        *ptimeout = apr_time_from_sec(n * MD_SECS_PER_DAY);
+        return APR_SUCCESS;
+    }
+    else {
+        def_unit = endp;
+    }
+    rv = ap_timeout_parameter_parse(value, ptimeout, def_unit);
+    if (APR_SUCCESS == rv && funits > 1) {
+        *ptimeout *= funits;
+    }
+    return rv;
+}
+
+static apr_status_t percentage_parse(const char *value, int *ppercent)
+{
+    char *endp;
+    apr_int64_t n;
+    
+    n = apr_strtoi64(value, &endp, 10);
+    if (errno) {
+        return errno;
+    }
+    if (*endp == '%') {
+        if (n < 0 || n >= 100) {
+            return APR_BADARG;
+        }
+        *ppercent = (int)n;
+        return APR_SUCCESS;
+    }
+    return APR_EINVAL;
+}
+
+static const char *md_config_set_renew_window(cmd_parms *cmd, void *dc, const char *value)
+{
+    md_srv_conf_t *config = md_config_get(cmd->server);
+    const char *err;
+    apr_interval_time_t timeout;
+    int percent = 0;
+    
+    (void)dc;
+    if (!inside_section(cmd, MD_CMD_MD_SECTION)
+        && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) {
+        return err;
+    }
+
+    /* Inspired by http_core.c */
+    if (duration_parse(value, &timeout, "d") == APR_SUCCESS) {
+        config->renew_norm = 0;
+        config->renew_window = timeout;
+        return NULL;
+    }
+    else {
+        switch (percentage_parse(value, &percent)) {
+            case APR_SUCCESS:
+                config->renew_norm = apr_time_from_sec(100 * MD_SECS_PER_DAY);
+                config->renew_window = apr_time_from_sec(percent * MD_SECS_PER_DAY);
+                return NULL;
+            case APR_BADARG:
+                return "MDRenewWindow as percent must be less than 100";
+        }
+    }
+    return "MDRenewWindow has unrecognized format";
+}
+
+static const char *md_config_set_proxy(cmd_parms *cmd, void *arg, const char *value)
+{
+    md_srv_conf_t *sc = md_config_get(cmd->server);
+    const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY);
+
+    if (err) {
+        return err;
+    }
+    md_util_abs_http_uri_check(cmd->pool, value, &err);
+    if (err) {
+        return err;
+    }
+    sc->mc->proxy_url = value;
+    (void)arg;
+    return NULL;
+}
+
+static const char *md_config_set_store_dir(cmd_parms *cmd, void *arg, const char *value)
+{
+    md_srv_conf_t *sc = md_config_get(cmd->server);
+    const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY);
+
+    if (err) {
+        return err;
+    }
+    sc->mc->base_dir = value;
+    (void)arg;
+    return NULL;
+}
+
+static const char *set_port_map(md_mod_conf_t *mc, const char *value)
+{
+    int net_port, local_port;
+    char *endp;
+
+    net_port = (int)apr_strtoi64(value, &endp, 10);
+    if (errno) {
+        return "unable to parse first port number";
+    }
+    if (!endp || *endp != ':') {
+        return "no ':' after first port number";
+    }
+    ++endp;
+    if (*endp == '-') {
+        local_port = 0;
+    }
+    else {
+        local_port = (int)apr_strtoi64(endp, &endp, 10);
+        if (errno) {
+            return "unable to parse second port number";
+        }
+        if (local_port <= 0 || local_port > 65535) {
+            return "invalid number for port map, must be in ]0,65535]";
+        }
+    }
+    switch (net_port) {
+        case 80:
+            mc->local_80 = local_port;
+            break;
+        case 443:
+            mc->local_443 = local_port;
+            break;
+        default:
+            return "mapped port number must be 80 or 443";
+    }
+    return NULL;
+}
+
+static const char *md_config_set_port_map(cmd_parms *cmd, void *arg, 
+                                          const char *v1, const char *v2)
+{
+    md_srv_conf_t *sc = md_config_get(cmd->server);
+    const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY);
+
+    (void)arg;
+    if (!err) {
+        err = set_port_map(sc->mc, v1);
+    }
+    if (!err && v2) {
+        err = set_port_map(sc->mc, v2);
+    }
+    return err;
+}
+
+static const char *md_config_set_cha_tyes(cmd_parms *cmd, void *dc, 
+                                          int argc, char *const argv[])
+{
+    md_srv_conf_t *config = md_config_get(cmd->server);
+    apr_array_header_t **pcha, *ca_challenges;
+    const char *err;
+    int i;
+
+    (void)dc;
+    if (!inside_section(cmd, MD_CMD_MD_SECTION)
+        && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) {
+        return err;
+    }
+    pcha = &config->ca_challenges; 
+    
+    ca_challenges = *pcha;
+    if (!ca_challenges) {
+        *pcha = ca_challenges = apr_array_make(cmd->pool, 5, sizeof(const char *));
+    }
+    for (i = 0; i < argc; ++i) {
+        APR_ARRAY_PUSH(ca_challenges, const char *) = argv[i];
+    }
+    
+    return NULL;
+}
+
+static const char *md_config_set_pkeys(cmd_parms *cmd, void *dc, 
+                                       int argc, char *const argv[])
+{
+    md_srv_conf_t *config = md_config_get(cmd->server);
+    const char *err, *ptype;
+    apr_int64_t bits;
+    
+    (void)dc;
+    if (!inside_section(cmd, MD_CMD_MD_SECTION)
+        && (err = ap_check_cmd_context(cmd, GLOBAL_ONLY))) {
+        return err;
+    }
+    if (argc <= 0) {
+        return "needs to specify the private key type";
+    }
+    
+    ptype = argv[0];
+    if (!apr_strnatcasecmp("Default", ptype)) {
+        if (argc > 1) {
+            return "type 'Default' takes no parameter";
+        }
+        if (!config->pkey_spec) {
+            config->pkey_spec = apr_pcalloc(cmd->pool, sizeof(*config->pkey_spec));
+        }
+        config->pkey_spec->type = MD_PKEY_TYPE_DEFAULT;
+        return NULL;
+    }
+    else if (!apr_strnatcasecmp("RSA", ptype)) {
+        if (argc == 1) {
+            bits = MD_PKEY_RSA_BITS_DEF;
+        }
+        else if (argc == 2) {
+            bits = (int)apr_atoi64(argv[1]);
+            if (bits < MD_PKEY_RSA_BITS_MIN || bits >= INT_MAX) {
+                return apr_psprintf(cmd->pool, "must be %d or higher in order to be considered "
+                "safe. Too large a value will slow down everything. Larger then 4096 probably does "
+                "not make sense unless quantum cryptography really changes spin.", 
+                MD_PKEY_RSA_BITS_MIN);
+            }
+        }
+        else {
+            return "key type 'RSA' has only one optional parameter, the number of bits";
+        }
+
+        if (!config->pkey_spec) {
+            config->pkey_spec = apr_pcalloc(cmd->pool, sizeof(*config->pkey_spec));
+        }
+        config->pkey_spec->type = MD_PKEY_TYPE_RSA;
+        config->pkey_spec->params.rsa.bits = (unsigned int)bits;
+        return NULL;
+    }
+    return apr_pstrcat(cmd->pool, "unsupported private key type \"", ptype, "\"", NULL);
+}
+
+static const char *md_config_set_notify_cmd(cmd_parms *cmd, void *arg, const char *value)
+{
+    md_srv_conf_t *sc = md_config_get(cmd->server);
+    const char *err = ap_check_cmd_context(cmd, GLOBAL_ONLY);
+
+    if (err) {
+        return err;
+    }
+    sc->mc->notify_cmd = value;
+    (void)arg;
+    return NULL;
+}
+
+const command_rec md_cmds[] = {
+    AP_INIT_TAKE1(     MD_CMD_CA, md_config_set_ca, NULL, RSRC_CONF, 
+                  "URL of CA issuing the certificates"),
+    AP_INIT_TAKE1(     MD_CMD_CAAGREEMENT, md_config_set_agreement, NULL, RSRC_CONF, 
+                  "URL of CA Terms-of-Service agreement you accept"),
+    AP_INIT_TAKE_ARGV( MD_CMD_CACHALLENGES, md_config_set_cha_tyes, NULL, RSRC_CONF, 
+                      "A list of challenge types to be used."),
+    AP_INIT_TAKE1(     MD_CMD_CAPROTO, md_config_set_ca_proto, NULL, RSRC_CONF, 
+                  "Protocol used to obtain/renew certificates"),
+    AP_INIT_TAKE1(     MD_CMD_DRIVEMODE, md_config_set_drive_mode, NULL, RSRC_CONF, 
+                  "method of obtaining certificates for the managed domain"),
+    AP_INIT_TAKE_ARGV( MD_CMD_MD, md_config_set_names, NULL, RSRC_CONF, 
+                      "A group of server names with one certificate"),
+    AP_INIT_RAW_ARGS(  MD_CMD_MD_SECTION, md_config_sec_start, NULL, RSRC_CONF, 
+                     "Container for a managed domain with common settings and certificate."),
+    AP_INIT_TAKE_ARGV( MD_CMD_MEMBER, md_config_sec_add_members, NULL, RSRC_CONF, 
+                      "Define domain name(s) part of the Managed Domain. Use 'auto' or "
+                      "'manual' to enable/disable auto adding names from virtual hosts."),
+    AP_INIT_TAKE_ARGV( MD_CMD_MEMBERS, md_config_sec_add_members, NULL, RSRC_CONF, 
+                      "Define domain name(s) part of the Managed Domain. Use 'auto' or "
+                      "'manual' to enable/disable auto adding names from virtual hosts."),
+    AP_INIT_TAKE1(     MD_CMD_MUSTSTAPLE, md_config_set_must_staple, NULL, RSRC_CONF, 
+                  "Enable/Disable the Must-Staple flag for new certificates."),
+    AP_INIT_TAKE12(    MD_CMD_PORTMAP, md_config_set_port_map, NULL, RSRC_CONF, 
+                  "Declare the mapped ports 80 and 443 on the local server. E.g. 80:8000 "
+                  "to indicate that the server port 8000 is reachable as port 80 from the "
+                  "internet. Use 80:- to indicate that port 80 is not reachable from "
+                  "the outside."),
+    AP_INIT_TAKE_ARGV( MD_CMD_PKEYS, md_config_set_pkeys, NULL, RSRC_CONF, 
+                  "set the type and parameters for private key generation"),
+    AP_INIT_TAKE1(     MD_CMD_PROXY, md_config_set_proxy, NULL, RSRC_CONF, 
+                  "URL of a HTTP(S) proxy to use for outgoing connections"),
+    AP_INIT_TAKE1(     MD_CMD_STOREDIR, md_config_set_store_dir, NULL, RSRC_CONF, 
+                  "the directory for file system storage of managed domain data."),
+    AP_INIT_TAKE1(     MD_CMD_RENEWWINDOW, md_config_set_renew_window, NULL, RSRC_CONF, 
+                  "Time length for renewal before certificate expires (defaults to days)"),
+    AP_INIT_TAKE1(     MD_CMD_REQUIREHTTPS, md_config_set_require_https, NULL, RSRC_CONF, 
+                  "Redirect non-secure requests to the https: equivalent."),
+    AP_INIT_TAKE1(     MD_CMD_NOTIFYCMD, md_config_set_notify_cmd, NULL, RSRC_CONF, 
+                  "set the command to run when signup/renew of domain is complete."),
+    AP_INIT_TAKE1(NULL, NULL, NULL, RSRC_CONF, NULL)
+};
+
+apr_status_t md_config_post_config(server_rec *s, apr_pool_t *p)
+{
+    md_srv_conf_t *sc;
+    md_mod_conf_t *mc;
+
+    sc = md_config_get(s);
+    mc = sc->mc;
+
+    mc->hsts_header = NULL;
+    if (mc->hsts_max_age > 0) {
+        mc->hsts_header = apr_psprintf(p, "max-age=%d", mc->hsts_max_age);
+    }
+    
+    return APR_SUCCESS;
+}
+
+static md_srv_conf_t *config_get_int(server_rec *s, apr_pool_t *p)
+{
+    md_srv_conf_t *sc = (md_srv_conf_t *)ap_get_module_config(s->module_config, &md_module);
+    ap_assert(sc);
+    if (sc->s != s && p) {
+        sc = md_config_merge(p, &defconf, sc);
+        sc->name = apr_pstrcat(p, CONF_S_NAME(s), sc->name, NULL);
+        sc->mc = md_mod_conf_get(p, 1);
+        ap_set_module_config(s->module_config, &md_module, sc);
+    }
+    return sc;
+}
+
+md_srv_conf_t *md_config_get(server_rec *s)
+{
+    return config_get_int(s, NULL);
+}
+
+md_srv_conf_t *md_config_get_unique(server_rec *s, apr_pool_t *p)
+{
+    assert(p);
+    return config_get_int(s, p);
+}
+
+md_srv_conf_t *md_config_cget(conn_rec *c)
+{
+    return md_config_get(c->base_server);
+}
+
+const char *md_config_gets(const md_srv_conf_t *sc, md_config_var_t var)
+{
+    switch (var) {
+        case MD_CONFIG_CA_URL:
+            return sc->ca_url? sc->ca_url : defconf.ca_url;
+        case MD_CONFIG_CA_PROTO:
+            return sc->ca_proto? sc->ca_proto : defconf.ca_proto;
+        case MD_CONFIG_BASE_DIR:
+            return sc->mc->base_dir;
+        case MD_CONFIG_PROXY:
+            return sc->mc->proxy_url;
+        case MD_CONFIG_CA_AGREEMENT:
+            return sc->ca_agreement? sc->ca_agreement : defconf.ca_agreement;
+        case MD_CONFIG_NOTIFY_CMD:
+            return sc->mc->notify_cmd;
+        default:
+            return NULL;
+    }
+}
+
+int md_config_geti(const md_srv_conf_t *sc, md_config_var_t var)
+{
+    switch (var) {
+        case MD_CONFIG_DRIVE_MODE:
+            return (sc->drive_mode != DEF_VAL)? sc->drive_mode : defconf.drive_mode;
+        case MD_CONFIG_LOCAL_80:
+            return sc->mc->local_80;
+        case MD_CONFIG_LOCAL_443:
+            return sc->mc->local_443;
+        case MD_CONFIG_TRANSITIVE:
+            return (sc->transitive != DEF_VAL)? sc->transitive : defconf.transitive;
+        case MD_CONFIG_REQUIRE_HTTPS:
+            return (sc->require_https != MD_REQUIRE_UNSET)? sc->require_https : defconf.require_https;
+        case MD_CONFIG_MUST_STAPLE:
+            return (sc->must_staple != DEF_VAL)? sc->must_staple : defconf.must_staple;
+        default:
+            return 0;
+    }
+}
+
+apr_interval_time_t md_config_get_interval(const md_srv_conf_t *sc, md_config_var_t var)
+{
+    switch (var) {
+        case MD_CONFIG_RENEW_NORM:
+            return (sc->renew_norm != DEF_VAL)? sc->renew_norm : defconf.renew_norm;
+        case MD_CONFIG_RENEW_WINDOW:
+            return (sc->renew_window != DEF_VAL)? sc->renew_window : defconf.renew_window;
+        default:
+            return 0;
+    }
+}
diff --git a/modules/md/mod_md_config.h b/modules/md/mod_md_config.h
new file mode 100644 (file)
index 0000000..2b23634
--- /dev/null
@@ -0,0 +1,100 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef mod_md_md_config_h
+#define mod_md_md_config_h
+
+struct md_store_t;
+struct md_reg_t;
+struct md_pkey_spec_t;
+
+typedef enum {
+    MD_CONFIG_CA_URL,
+    MD_CONFIG_CA_PROTO,
+    MD_CONFIG_BASE_DIR,
+    MD_CONFIG_CA_AGREEMENT,
+    MD_CONFIG_DRIVE_MODE,
+    MD_CONFIG_LOCAL_80,
+    MD_CONFIG_LOCAL_443,
+    MD_CONFIG_RENEW_NORM,
+    MD_CONFIG_RENEW_WINDOW,
+    MD_CONFIG_TRANSITIVE,
+    MD_CONFIG_PROXY,
+    MD_CONFIG_REQUIRE_HTTPS,
+    MD_CONFIG_MUST_STAPLE,
+    MD_CONFIG_NOTIFY_CMD,
+} md_config_var_t;
+
+typedef struct {
+    apr_array_header_t *mds;           /* all md_t* defined in the config, shared */
+    const char *base_dir;              /* base dir for store */
+    const char *proxy_url;             /* proxy url to use (or NULL) */
+    struct md_reg_t *reg;              /* md registry instance, singleton, shared */
+
+    int local_80;                      /* On which port http:80 arrives */
+    int local_443;                     /* On which port https:443 arrives */
+    int can_http;                      /* Does someone listen to the local port 80 equivalent? */
+    int can_https;                     /* Does someone listen to the local port 443 equivalent? */
+    int hsts_max_age;                  /* max-age of HSTS (rfc6797) header */
+    const char *hsts_header;           /* computed HTST header to use or NULL */
+    apr_array_header_t *unused_names;  /* post config, names of all MDs not assigned to a vhost */
+
+    const char *notify_cmd;            /* notification command to execute on signup/renew */
+} md_mod_conf_t;
+
+typedef struct md_srv_conf_t {
+    const char *name;
+    const server_rec *s;               /* server this config belongs to */
+    md_mod_conf_t *mc;                 /* global config settings */
+    
+    int transitive;                    /* != 0 iff VirtualHost names/aliases are auto-added */
+    md_require_t require_https;        /* If MDs require https: access */
+    int drive_mode;                    /* mode of obtaining credentials */
+    int must_staple;                   /* certificates should set the OCSP Must Staple extension */
+    struct md_pkey_spec_t *pkey_spec;  /* specification for generating private keys */
+    apr_interval_time_t renew_norm;    /* If > 0, use as normalizing value for cert lifetime
+                                        * Example: renew_norm=90d renew_win=30d, cert lives
+                                        * for 12 days => renewal 4 days before */
+    apr_interval_time_t renew_window;  /* time before expiration that starts renewal */
+    
+    const char *ca_url;                /* url of CA certificate service */
+    const char *ca_proto;              /* protocol used vs CA (e.g. ACME) */
+    const char *ca_agreement;          /* accepted agreement uri between CA and user */ 
+    struct apr_array_header_t *ca_challenges; /* challenge types configured */
+
+    md_t *current;                     /* md currently defined in <ManagedDomain xxx> section */
+    md_t *assigned;                    /* post_config: MD that applies to this server or NULL */
+} md_srv_conf_t;
+
+void *md_config_create_svr(apr_pool_t *pool, server_rec *s);
+void *md_config_merge_svr(apr_pool_t *pool, void *basev, void *addv);
+
+extern const command_rec md_cmds[];
+
+apr_status_t md_config_post_config(server_rec *s, apr_pool_t *p);
+
+/* Get the effective md configuration for the connection */
+md_srv_conf_t *md_config_cget(conn_rec *c);
+/* Get the effective md configuration for the server */
+md_srv_conf_t *md_config_get(server_rec *s);
+/* Get the effective md configuration for the server, but make it
+ * unique to this server_rec, so that any changes only affect this server */
+md_srv_conf_t *md_config_get_unique(server_rec *s, apr_pool_t *p);
+
+const char *md_config_gets(const md_srv_conf_t *config, md_config_var_t var);
+int md_config_geti(const md_srv_conf_t *config, md_config_var_t var);
+apr_interval_time_t md_config_get_interval(const md_srv_conf_t *config, md_config_var_t var);
+
+#endif /* md_config_h */
diff --git a/modules/md/mod_md_os.c b/modules/md/mod_md_os.c
new file mode 100644 (file)
index 0000000..8799735
--- /dev/null
@@ -0,0 +1,88 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <assert.h>
+#include <apr_strings.h>
+
+#ifndef AP_ENABLE_EXCEPTION_HOOK
+#define AP_ENABLE_EXCEPTION_HOOK 0
+#endif
+
+#include <mpm_common.h>
+#include <httpd.h>
+#include <http_log.h>
+#include <ap_mpm.h>
+
+#if APR_HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+#ifdef WIN32
+#include "mpm_winnt.h"
+#endif
+#if AP_NEED_SET_MUTEX_PERMS
+#include "unixd.h"
+#endif
+
+#include "md_util.h"
+#include "mod_md_os.h"
+
+apr_status_t md_try_chown(const char *fname, unsigned int uid, int gid, apr_pool_t *p)
+{
+#if AP_NEED_SET_MUTEX_PERMS
+    if (-1 == chown(fname, (uid_t)uid, (gid_t)gid)) {
+        apr_status_t rv = APR_FROM_OS_ERROR(errno);
+        if (!APR_STATUS_IS_ENOENT(rv)) {
+            ap_log_perror(APLOG_MARK, APLOG_ERR, rv, p, APLOGNO(10082)
+                         "Can't change owner of %s", fname);
+        }
+        return rv;
+    }
+    return APR_SUCCESS;
+#else 
+    return APR_ENOTIMPL;
+#endif
+}
+
+apr_status_t md_make_worker_accessible(const char *fname, apr_pool_t *p)
+{
+#if AP_NEED_SET_MUTEX_PERMS
+    return md_try_chown(fname, ap_unixd_config.user_id, -1, p);
+#else 
+    return APR_ENOTIMPL;
+#endif
+}
+
+#ifdef WIN32
+
+apr_status_t md_server_graceful(apr_pool_t *p, server_rec *s)
+{
+    return APR_ENOTIMPL;
+}
+#else
+
+apr_status_t md_server_graceful(apr_pool_t *p, server_rec *s)
+{ 
+    apr_status_t rv;
+    
+    (void)p;
+    (void)s;
+    rv = (kill(getppid(), AP_SIG_GRACEFUL) < 0)? APR_ENOTIMPL : APR_SUCCESS;
+    ap_log_error(APLOG_MARK, APLOG_TRACE1, errno, NULL, "sent signal to parent");
+    return rv;
+}
+
+#endif
+
diff --git a/modules/md/mod_md_os.h b/modules/md/mod_md_os.h
new file mode 100644 (file)
index 0000000..b4b59d8
--- /dev/null
@@ -0,0 +1,36 @@
+/* Copyright 2017 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef mod_md_md_os_h
+#define mod_md_md_os_h
+
+/**
+ * Try chown'ing the file/directory. Give id -1 to not change uid/gid.
+ * Will return APR_ENOTIMPL on platforms not supporting this operation.
+ */
+apr_status_t md_try_chown(const char *fname, unsigned int uid, int gid, apr_pool_t *p);
+
+/**
+ * Make a file or directory read/write(/searchable) by httpd workers.
+ */
+apr_status_t md_make_worker_accessible(const char *fname, apr_pool_t *p);
+
+/**
+ * Trigger a graceful restart of the server. Depending on the architecture, may
+ * return APR_ENOTIMPL.
+ */
+apr_status_t md_server_graceful(apr_pool_t *p, server_rec *s);
+
+#endif /* mod_md_md_os_h */
diff --git a/modules/md/mod_md_private.h b/modules/md/mod_md_private.h
new file mode 100644 (file)
index 0000000..d4a4a76
--- /dev/null
@@ -0,0 +1,23 @@
+/* Copyright 2015 greenbytes GmbH (https://www.greenbytes.de)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef mod_md_md_private_h
+#define mod_md_md_private_h
+
+extern module AP_MODULE_DECLARE_DATA md_module;
+
+APLOG_USE_MODULE(md);
+
+#endif
index e5dee0fc41232778ff95a43719206d328e113353..6f7dd33b7c8306924fe563eaecb288ff5e693455 100644 (file)
@@ -30,6 +30,7 @@
 #include "mod_ssl.h"
 #include "mod_ssl_openssl.h"
 #include "mpm_common.h"
+#include "mod_md.h"
 
 APR_IMPLEMENT_OPTIONAL_HOOK_RUN_ALL(ssl, SSL, int, init_server,
                                     (server_rec *s,apr_pool_t *p,int is_proxy,SSL_CTX *ctx),
@@ -164,6 +165,24 @@ static void ssl_add_version_components(apr_pool_t *p,
                  modver, AP_SERVER_BASEVERSION, incver);
 }
 
+/**************************************************************************************************/
+/* Managed Domains Interface */
+
+static APR_OPTIONAL_FN_TYPE(md_is_managed) *md_is_managed;
+static APR_OPTIONAL_FN_TYPE(md_get_certificate) *md_get_certificate;
+static APR_OPTIONAL_FN_TYPE(md_is_challenge) *md_is_challenge;
+
+int ssl_is_challenge(conn_rec *c, const char *servername, 
+                     X509 **pcert, EVP_PKEY **pkey)
+{
+    if (md_is_challenge) {
+        return md_is_challenge(c, servername, pcert, pkey);
+    }
+    *pcert = NULL;
+    *pkey = NULL;
+    return 0;
+}
+
 /*
  *  Per-module initialization
  */
@@ -204,6 +223,16 @@ apr_status_t ssl_init_Module(apr_pool_t *p, apr_pool_t *plog,
     ssl_config_global_create(base_server); /* just to avoid problems */
     ssl_config_global_fix(mc);
 
+    /* Initialize our interface to mod_md, if it is loaded 
+     */
+    md_is_managed = APR_RETRIEVE_OPTIONAL_FN(md_is_managed);
+    md_get_certificate = APR_RETRIEVE_OPTIONAL_FN(md_get_certificate);
+    md_is_challenge = APR_RETRIEVE_OPTIONAL_FN(md_is_challenge);
+    if (!md_is_managed || !md_get_certificate) {
+        md_is_managed = NULL;
+        md_get_certificate = NULL;
+    }
+
     /*
      *  try to fix the configuration and open the dedicated SSL
      *  logfile as early as possible
@@ -1606,6 +1635,52 @@ static apr_status_t ssl_init_server_ctx(server_rec *s,
         return APR_EGENERAL;
     }
 
+    ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, APLOGNO(10083)
+                 "Init: (%s) mod_md support is %s.", ssl_util_vhostid(p, s),
+                 md_is_managed? "available" : "unavailable");
+    if (md_is_managed && md_is_managed(s)) {
+        modssl_pk_server_t *const pks = sc->server->pks;
+        if (pks->cert_files->nelts > 0 || pks->key_files->nelts > 0) {
+            ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s, APLOGNO(10084)
+                         "Init: (%s) You configured certificate/key files on this host, but "
+                         "is is covered by a Managed Domain. You need to remove these directives "
+                         "for the Managed Domain to take over.", ssl_util_vhostid(p, s));
+        }
+        else {
+            const char *key_file, *cert_file, *chain_file;
+            
+            key_file = cert_file = chain_file = NULL;
+            
+            if (md_get_certificate) {
+                rv = md_get_certificate(s, p, &key_file, &cert_file);
+            }
+            else {
+                rv = APR_ENOTIMPL;
+            }
+            
+            if (key_file && cert_file) {
+                ap_log_error(APLOG_MARK, APLOG_TRACE1, 0, s, 
+                             "%s: installing key=%s, cert=%s, chain=%s", 
+                             ssl_util_vhostid(p, s), key_file, cert_file, chain_file);
+                APR_ARRAY_PUSH(pks->key_files, const char *) = key_file;
+                APR_ARRAY_PUSH(pks->cert_files, const char *) = cert_file;
+                sc->server->cert_chain = chain_file;
+            }
+            
+            if (APR_STATUS_IS_EAGAIN(rv)) {
+                /* Managed Domain not ready yet. This is not a reason to fail the config */
+                ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s, APLOGNO(10085)
+                             "Init: %s will respond with '503 Service Unavailable' for now. This "
+                             "host is part of a Managed Domain, but no SSL certificate is "
+                             "available (yet).", ssl_util_vhostid(p, s));
+                pks->service_unavailable = 1;
+            }
+            else if (rv != APR_SUCCESS) {
+                return rv;
+            }
+        }
+    }
+    
     if ((rv = ssl_init_ctx(s, p, ptemp, sc->server)) != APR_SUCCESS) {
         return rv;
     }
index 7c00123088ca83bf12a0621fe1939993f0fe6659..b22f8c125a3eb9dd98c392f05ee2aab5f88d21cb 100644 (file)
@@ -264,6 +264,15 @@ int ssl_hook_ReadReq(request_rec *r)
         return DECLINED;
     }
 
+    if (sslconn->service_unavailable) {
+        /* This is set when the SSL properties of this connection are
+         * incomplete or if this connection was made to challenge a 
+         * particular hostname (ACME). We never serve any request on 
+         * such a connection. */
+         /* TODO: a retry-after indicator would be nice here */
+        return HTTP_SERVICE_UNAVAILABLE;
+    }
+
     if (sslconn->non_ssl_request == NON_SSL_SET_ERROR_MSG) {
         apr_table_setn(r->notes, "error-notes",
                        "Reason: You're speaking plain HTTP to an SSL-enabled "
@@ -2110,6 +2119,8 @@ void ssl_callback_Info(const SSL *ssl, int where, int rc)
 static apr_status_t init_vhost(conn_rec *c, SSL *ssl)
 {
     const char *servername;
+    X509 *cert;
+    EVP_PKEY *key;
     
     if (c) {
         SSLConnRec *sslcon = myConnConfig(c);
@@ -2126,8 +2137,35 @@ static apr_status_t init_vhost(conn_rec *c, SSL *ssl)
                 ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(02043)
                               "SSL virtual host for servername %s found",
                               servername);
+                
                 return APR_SUCCESS;
             }
+            else if (ssl_is_challenge(c, servername, &cert, &key)) {
+            
+                sslcon->service_unavailable = 1;
+                if ((SSL_use_certificate(ssl, cert) < 1)) {
+                    ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, c, APLOGNO(10086)
+                                  "Failed to configure challenge certificate %s",
+                                  servername);
+                    return APR_EGENERAL;
+                }
+                
+                if (!SSL_use_PrivateKey(ssl, key)) {
+                    ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, c, APLOGNO(10087)
+                                  "error '%s' using Challenge key: %s",
+                                  ERR_error_string(ERR_peek_last_error(), NULL), 
+                                  servername);
+                    return APR_EGENERAL;
+                }
+                
+                if (SSL_check_private_key(ssl) < 1) {
+                    ap_log_cerror(APLOG_MARK, APLOG_WARNING, 0, c, APLOGNO(10088)
+                                  "Challenbge certificate and private key %s "
+                                  "do not match", servername);
+                    return APR_EGENERAL;
+                }
+                
+            }
             else {
                 ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(02044)
                               "No matching SSL virtual host for servername "
@@ -2233,6 +2271,8 @@ static int ssl_find_vhost(void *servername, conn_rec *c, server_rec *s)
          */
         sslcon->server = s;
         sslcon->cipher_suite = sc->server->auth.cipher_suite;
+        sslcon->service_unavailable = sc->server->pks? 
+            sc->server->pks->service_unavailable : 0; 
         
         ap_update_child_status_from_server(c->sbh, SERVER_BUSY_READ, c, s);
         /*
index 0ab60a0dfb74ada4056b68da299f57582ce3104a..9990f8b41e2da74ff1591dc2976bdb3257f325d1 100644 (file)
@@ -524,6 +524,7 @@ typedef struct {
     server_rec *server;
     
     const char *cipher_suite; /* cipher suite used in last reneg */
+    int service_unavailable;  /* thouugh we negotiate SSL, no requests will be served */
 } SSLConnRec;
 
 /* BIG FAT WARNING: SSLModConfigRec has unusual memory lifetime: it is
@@ -600,6 +601,9 @@ typedef struct {
      * sent in the CertificateRequest message: */
     const char  *ca_name_path;
     const char  *ca_name_file;
+    
+    /* TLS service for this server is suspended */
+    int service_unavailable;
 } modssl_pk_server_t;
 
 typedef struct {
@@ -1063,6 +1067,9 @@ void ssl_init_ocsp_certificates(server_rec *s, modssl_ctx_t *mctx);
  * memory. */
 DH *modssl_get_dh_params(unsigned keylen);
 
+int ssl_is_challenge(conn_rec *c, const char *servername, 
+                     X509 **pcert, EVP_PKEY **pkey);
+
 #endif /* SSL_PRIVATE_H */
 /** @} */
 
index 980759210d6121dbe1f99c16ea6966a1d00d01dd..e22dd7d6946d411da54147a383a330d97a265efd 100644 (file)
@@ -115,6 +115,33 @@ EVP_PKEY *modssl_read_privatekey(const char* filename, EVP_PKEY **key, pem_passw
     return rc;
 }
 
+typedef struct {
+    const char *pass;
+    int pass_len;
+} pass_ctx;
+
+static int provide_pass(char *buf, int size, int rwflag, void *baton)
+{
+    pass_ctx *ctx = baton;
+    if (ctx->pass_len > 0) {
+        if (ctx->pass_len < size) {
+            size = (int)ctx->pass_len;
+        }
+        memcpy(buf, ctx->pass, size);
+    }
+    return ctx->pass_len;
+}
+
+EVP_PKEY   *modssl_read_encrypted_pkey(const char *filename, EVP_PKEY **key, 
+                                       const char *pass, apr_size_t pass_len)
+{
+    pass_ctx ctx;
+    
+    ctx.pass = pass;
+    ctx.pass_len = pass_len;
+    return modssl_read_privatekey(filename, key, provide_pass, &ctx);
+}
+
 /*  _________________________________________________________________
 **
 **  Smart shutdown
index 04b138d2aea5d62e28ec67b1c87b18c1be5c577a..5c6c8b6853101dac3ab6a6d50ef346958ec2649b 100644 (file)
@@ -65,6 +65,7 @@ void        modssl_init_app_data2_idx(void);
 void       *modssl_get_app_data2(SSL *);
 void        modssl_set_app_data2(SSL *, void *);
 EVP_PKEY   *modssl_read_privatekey(const char *, EVP_PKEY **, pem_password_cb *, void *);
+EVP_PKEY   *modssl_read_encrypted_pkey(const char *, EVP_PKEY **, const char *, apr_size_t);
 int         modssl_smart_shutdown(SSL *ssl);
 BOOL        modssl_X509_getBC(X509 *, int *, int *);
 char       *modssl_X509_NAME_ENTRY_to_string(apr_pool_t *p, X509_NAME_ENTRY *xsne);