[rs-commit] r540 - in /mod_ca/trunk: ChangeLog mod_ca_disk.c

rs-commit at redwax.eu rs-commit at redwax.eu
Fri Mar 6 14:49:59 CET 2026


Author: minfrin at redwax.eu
Date: Fri Mar  6 14:49:58 2026
New Revision: 540

Log:
Add ca_reqauthz hook to mod_ca_disk to verify the case
where the renewal or reissue was signed by a 
previously issued certificate.

Modified:
    mod_ca/trunk/ChangeLog
    mod_ca/trunk/mod_ca_disk.c

Modified: mod_ca/trunk/ChangeLog
==============================================================================
--- mod_ca/trunk/ChangeLog	(original)
+++ mod_ca/trunk/ChangeLog	Fri Mar  6 14:49:58 2026
@@ -1,5 +1,9 @@
 
 Changes with v1.0.0
+
+ *) Add ca_reqauthz hook to mod_ca_disk to verify the case
+    where the renewal or reissue was signed by a
+    previously issued certificate. [Graham Leggett]
 
  *) Add constants for proof of possession. [Graham Leggett]
 

Modified: mod_ca/trunk/mod_ca_disk.c
==============================================================================
--- mod_ca/trunk/mod_ca_disk.c	(original)
+++ mod_ca/trunk/mod_ca_disk.c	Fri Mar  6 14:49:58 2026
@@ -207,6 +207,12 @@
     return APR_SUCCESS;
 }
 
+static apr_status_t ca_X509_cleanup(void *data)
+{
+    X509_free((X509 *) data);
+    return APR_SUCCESS;
+}
+
 static apr_status_t ca_BIO_cleanup(void *data)
 {
     BIO_free((BIO *) data);
@@ -237,17 +243,36 @@
     return APR_SUCCESS;
 }
 
+static apr_status_t ca_OPENSSL_cleanup(void *data)
+{
+    OPENSSL_free(data);
+    return APR_SUCCESS;
+}
+
 static apr_status_t ca_TXT_DB_cleanup(void *data)
 {
     TXT_DB_free((TXT_DB *) data);
     return APR_SUCCESS;
 }
 
+static ca_asn1_t *make_X509(apr_pool_t *pool, X509 *cert)
+{
+    ca_asn1_t *buf = apr_palloc(pool, sizeof(ca_asn1_t));
+    unsigned char *tmp;
+
+    buf->len = i2d_X509(cert, NULL);
+    buf->val = tmp = apr_palloc(pool, buf->len);
+    i2d_X509(cert, &tmp);
+
+    return buf;
+}
+
 static ASN1_STRING *parse_ASN1_STRING(apr_pool_t *pool, ca_asn1_t *string)
 {
     ASN1_STRING *s = NULL;
     if (string) {
-        d2i_ASN1_PRINTABLE(&s, &string->val, string->len);
+        const unsigned char *val = string->val;
+        d2i_ASN1_PRINTABLE(&s, &val, string->len);
         if (s) {
             apr_pool_cleanup_register(pool, s, ca_ASN1_STRING_cleanup,
                     apr_pool_cleanup_null);
@@ -260,7 +285,8 @@
 {
     ASN1_TIME *t = NULL;
     if (time) {
-        d2i_ASN1_TIME(&t, &time->val, time->len);
+        const unsigned char *val = time->val;
+        d2i_ASN1_TIME(&t, &val, time->len);
         if (t) {
             apr_pool_cleanup_register(pool, t, ca_ASN1_TIME_cleanup,
                     apr_pool_cleanup_null);
@@ -273,7 +299,8 @@
 {
     ASN1_INTEGER *i = NULL;
     if (integer) {
-        d2i_ASN1_INTEGER(&i, &integer->val, integer->len);
+        const unsigned char *val = integer->val;
+        d2i_ASN1_INTEGER(&i, &val, integer->len);
         if (i) {
             apr_pool_cleanup_register(pool, i, ca_ASN1_INTEGER_cleanup,
                     apr_pool_cleanup_null);
@@ -286,13 +313,28 @@
 {
     X509_NAME *n = NULL;
     if (name) {
-        d2i_X509_NAME(&n, &name->val, name->len);
+        const unsigned char *val = name->val;
+        d2i_X509_NAME(&n, &val, name->len);
         if (n) {
             apr_pool_cleanup_register(pool, n, ca_X509_NAME_cleanup,
                     apr_pool_cleanup_null);
         }
     }
     return n;
+}
+
+static X509 *parse_X509(apr_pool_t *pool, ca_asn1_t *cert)
+{
+    X509 *x = NULL;
+    if (cert) {
+        const unsigned char *val = cert->val;
+        d2i_X509(&x, &val, cert->len);
+        if (x) {
+            apr_pool_cleanup_register(pool, x, ca_X509_cleanup,
+                    apr_pool_cleanup_null);
+        }
+    }
+    return x;
 }
 
 /* Prevent anything untowards in a filename; by replacing
@@ -300,31 +342,31 @@
  */
 static const char * safe_filename(apr_pool_t *pool, const char * fname) 
 {
-	const char hex[16] = "0123456789ABCDEF";
-	char * out = NULL, * o;
-
-	if (fname == NULL)
-		return "_";
-
-	if (!*fname)
-		return "_00";
-
-	// worst case lenght; including trailing \0.
-	out = apr_palloc(pool, 1 + strlen(fname) * 3);
+    const char hex[16] = "0123456789ABCDEF";
+    char * out = NULL, * o;
+
+    if (fname == NULL)
+        return "_";
+
+    if (!*fname)
+        return "_00";
+
+    // worst case lenght; including trailing \0.
+    out = apr_palloc(pool, 1 + strlen(fname) * 3);
 
         o = out;
-	for(const char *p = fname; *p; p++) 
-	{
-		if (apr_isalnum(*p)) 
-		{
-			*o++ = *p;
-			continue;
-		};
-		*o++ = '_';
-		*o++ = hex[ ( *p >> 4)  & 0xF ];
-		*o++ = hex[ ( *p >> 0)  & 0xF ];
-	};
-	return out;
+    for(const char *p = fname; *p; p++)
+    {
+        if (apr_isalnum(*p))
+        {
+            *o++ = *p;
+            continue;
+        };
+        *o++ = '_';
+        *o++ = hex[ ( *p >> 4)  & 0xF ];
+        *o++ = hex[ ( *p >> 0)  & 0xF ];
+    };
+    return out;
 }
 
 static int ca_sign_disk(request_rec *r, apr_hash_t *params,
@@ -564,19 +606,19 @@
                         APR_HASH_KEY_STRING) :
                         NULL;
         if (transaction_id) {
-	    const char * kid;
+        const char * kid;
             ASN1_STRING *s = parse_ASN1_STRING(r->pool, transaction_id);
             if (!s) {
                 log_message(r, status, "The transactionID could not be parsed");
 
                 return HTTP_BAD_REQUEST;
             }
-	    if (conf->serial_path)
-        	ap_log_rerror( APLOG_MARK, APLOG_WARNING, APR_SUCCESS, r, 
-			"Both serial and transaction path defined; using the latter.");
+        if (conf->serial_path)
+            ap_log_rerror( APLOG_MARK, APLOG_WARNING, APR_SUCCESS, r,
+            "Both serial and transaction path defined; using the latter.");
 
 #if HAVE_ASN1_STRING_GET0_DATA
-	    kid = (const char *) ASN1_STRING_get0_data(s);
+        kid = (const char *) ASN1_STRING_get0_data(s);
 #else
             kid = (const char *) ASN1_STRING_data(s);
 #endif
@@ -668,11 +710,11 @@
     /* do we have a link? create it now */
     if (link) {
 #if HAVE_APR_FILE_LINK
-    	status = apr_file_link(path, link);
+        status = apr_file_link(path, link);
 #else
         status = APR_ENOTIMPL;
 #endif
-    	if (APR_SUCCESS != status) {
+        if (APR_SUCCESS != status) {
             log_message(r, status,
                     "Could not link the certificate file to the CADiskCertificateByTransactionPath");
 
@@ -1257,6 +1299,222 @@
         apr_global_mutex_unlock(ca_disk_mutex);
         return OK;
     }
+}
+
+static int ca_reqauthz_disk(request_rec *r, apr_hash_t *params,
+        const unsigned char *buffer, apr_size_t len)
+{
+    ca_asn1_t *pop, *dpop;
+    X509 *cert, *dcert;
+    TXT_DB *db = NULL;
+    ASN1_INTEGER *si;
+    BIGNUM *bn = NULL;
+    char *fname;
+    char **result;
+    const char *key;
+    apr_status_t status;
+
+    ca_config_rec *conf = ap_get_module_config(r->per_dir_config,
+            &ca_disk_module);
+
+    /* index file defined? */
+    if (!conf->index_file || !conf->serial_path) {
+        return DECLINED;
+    }
+
+    pop = apr_hash_get(params, CA_POP_CERT, APR_HASH_KEY_STRING);
+
+    /* we only care about certificate proofs of possession */
+    if (!pop) {
+        return DECLINED;
+    }
+
+    cert = parse_X509(r->pool, pop);
+
+    if (!cert) {
+        log_message(r, APR_SUCCESS,
+                "could not DER decode the certificate");
+
+        return HTTP_BAD_REQUEST;
+    }
+
+    status = apr_global_mutex_lock(ca_disk_mutex);
+    if (APR_SUCCESS != status) {
+        log_message(r, status,
+                "Could not obtain the mutex for serial number index file");
+
+        return HTTP_INTERNAL_SERVER_ERROR;
+    }
+
+    /* read in the index file */
+    BIO *in = BIO_new(BIO_s_file());
+
+    if (BIO_read_filename(in, conf->index_file) <= 0) {
+        /* on read error, we create a database */
+        BIO_free(in);
+        in = BIO_new(BIO_s_mem());
+    }
+    apr_pool_cleanup_register(r->pool, in, ca_BIO_cleanup,
+            apr_pool_cleanup_null);
+
+    db = TXT_DB_read(in, DB_NUMBER);
+    if (!db) {
+        log_message(r, APR_SUCCESS, "Could not parse the index file");
+
+        apr_global_mutex_unlock(ca_disk_mutex);
+        return HTTP_INTERNAL_SERVER_ERROR;
+    }
+    apr_pool_cleanup_register(r->pool, db, ca_TXT_DB_cleanup,
+            apr_pool_cleanup_null);
+
+    apr_global_mutex_unlock(ca_disk_mutex);
+
+    if (!TXT_DB_create_index(db, DB_serial, NULL,
+            LHASH_HASH_FN(index_serial), LHASH_COMP_FN(index_serial))) {
+        log_message(r, APR_SUCCESS,
+                apr_psprintf(r->pool,
+                        "Could not hash the index file (%ld,%ld,%ld)",
+                        db->error, db->arg1, db->arg2));
+
+        return HTTP_INTERNAL_SERVER_ERROR;
+    }
+
+    /* isolate the serial number */
+    si = X509_get_serialNumber(cert);
+    if (!si) {
+        log_message(r, APR_SUCCESS,
+                "certificate had no serial number, renewal not possible");
+
+        return HTTP_BAD_REQUEST;
+    }
+    bn = ASN1_INTEGER_to_BN(si, NULL);
+    if (!bn) {
+        log_message(r, APR_SUCCESS,
+                "certificate serial number could not be converted to a big number, renewal not possible");
+
+        return HTTP_BAD_REQUEST;
+    }
+    apr_pool_cleanup_register(r->pool, bn, ca_BIGNUM_cleanup,
+            apr_pool_cleanup_null);
+
+    OPENSSL_STRING *row =
+            (char **) apr_pcalloc(r->pool, sizeof(char *)*(DB_NUMBER+1));
+    if (!row) {
+        log_message(r, APR_SUCCESS,
+                "Could not allocate search row for index file");
+
+        return HTTP_INTERNAL_SERVER_ERROR;
+    }
+
+    row[DB_serial] = BN_bn2hex(bn);
+    apr_pool_cleanup_register(r->pool, row[DB_serial], ca_OPENSSL_cleanup,
+            apr_pool_cleanup_null);
+
+    result = TXT_DB_get_by_index(db, DB_serial, row);
+
+    if (!result) {
+        log_message(r, APR_SUCCESS,
+                "Signing certificate serial number not found, renewal forbidden");
+
+        return HTTP_FORBIDDEN;
+    }
+
+    /* check certificate validity */
+
+    if (!result[DB_type] || result[DB_type][1]) {
+        /* none of the above */
+        log_message(r, APR_SUCCESS,
+                "Signing certificate has unknown status, renewal forbidden");
+
+        return HTTP_FORBIDDEN;
+    }
+
+    else if (result[DB_type][0] == DB_TYPE_VAL) {
+        /* valid, continue */
+    }
+    else if (result[DB_type][0] == DB_TYPE_REV) {
+        /* revoked */
+        log_message(r, APR_SUCCESS,
+                "Signing certificate revoked, renewal forbidden");
+
+        return HTTP_FORBIDDEN;
+    }
+    else if (result[DB_type][0] == DB_TYPE_EXP) {
+        /* expired */
+        log_message(r, APR_SUCCESS,
+                "Signing certificate expired, renewal forbidden");
+
+        return HTTP_FORBIDDEN;
+    }
+    else {
+        /* none of the above */
+        log_message(r, APR_SUCCESS,
+                "Signing certificate has unknown status, renewal forbidden");
+
+        return HTTP_FORBIDDEN;
+    }
+
+    /* check certificate expiry */
+
+    if (X509_cmp_current_time(X509_get0_notBefore(cert)) > 0) {
+        log_message(r, APR_SUCCESS,
+                "Signing certificate not yet valid, renewal forbidden");
+
+        return HTTP_FORBIDDEN;
+    }
+
+    if (X509_cmp_current_time(X509_get0_notAfter(cert)) < 0) {
+        log_message(r, APR_SUCCESS,
+                "Signing certificate expired, renewal forbidden");
+
+        return HTTP_FORBIDDEN;
+    }
+
+    /* try load the existing certificate from disk */
+
+    key = apr_pstrcat(r->pool, row[DB_serial], ".", conf->serial_path_suffix,
+            NULL);
+
+    status = apr_filepath_merge(&fname, conf->serial_path, key,
+            APR_FILEPATH_SECUREROOT | APR_FILEPATH_NOTRELATIVE, r->pool);
+    if (APR_SUCCESS != status) {
+        log_message(r, APR_SUCCESS,
+                "Signing certificate renewal forbidden");
+
+        return HTTP_FORBIDDEN;
+    }
+
+    in = BIO_new(BIO_s_file());
+    apr_pool_cleanup_register(r->pool, in, ca_BIO_cleanup,
+            apr_pool_cleanup_null);
+
+    if (BIO_read_filename(in, fname) <= 0) {
+        log_message(r, APR_SUCCESS,
+                "Signing certificate not found, renewal forbidden");
+
+        return HTTP_FORBIDDEN;
+    }
+
+    dcert = PEM_read_bio_X509_AUX(in, NULL, NULL, NULL);
+    if (!dcert) {
+        log_message(r, APR_SUCCESS,
+                "Signing certificate could not be parsed, renewal forbidden");
+
+        return HTTP_FORBIDDEN;
+    }
+
+    dpop = make_X509(r->pool, dcert);
+
+    if (!dpop || dpop->len != pop->len || memcmp(dpop->val, pop->val, dpop->len)) {
+        log_message(r, APR_SUCCESS,
+                "Signing certificate did not match, renewal forbidden");
+
+        return HTTP_FORBIDDEN;
+    }
+
+    /* if we got this far, the certs matched, were not revoked or expired */
+
+    return OK;
 }
 
 static void *create_ca_dir_config(apr_pool_t *p, char *d)
@@ -1481,6 +1739,7 @@
     ap_hook_child_init(ca_disk_child_init, NULL, NULL, APR_HOOK_MIDDLE);
     ap_hook_post_config(ca_disk_post_config, NULL, NULL, APR_HOOK_MIDDLE);
 
+    ap_hook_ca_reqauthz(ca_reqauthz_disk, NULL, NULL, APR_HOOK_MIDDLE);
     ap_hook_ca_sign(ca_sign_disk, NULL, NULL, APR_HOOK_MIDDLE);
     ap_hook_ca_certstore(ca_certstore_disk, NULL, NULL, APR_HOOK_MIDDLE);
     ap_hook_ca_getcert(ca_getcert_disk, NULL, NULL, APR_HOOK_MIDDLE);



More information about the rs-commit mailing list