[exim-cvs] EXTERNAL authenticator

Startseite
Nachricht löschen
Nachricht beantworten
Autor: Exim Git Commits Mailing List
Datum:  
To: exim-cvs
Betreff: [exim-cvs] EXTERNAL authenticator
Gitweb: https://git.exim.org/exim.git/commitdiff/b53c265b5c7825d7fb6bb672547c44d080459d71
Commit:     b53c265b5c7825d7fb6bb672547c44d080459d71
Parent:     37942ad83fcb6b788cceb4abe2891c92651811a7
Author:     Jeremy Harris <jgh146exb@???>
AuthorDate: Sat Jan 5 20:40:08 2019 +0000
Committer:  Jeremy Harris <jgh146exb@???>
CommitDate: Mon Feb 11 00:15:24 2019 +0000


    EXTERNAL authenticator


    (cherry picked from commit c4a8c663b74a35b547d8320547079ca56b3b772e)
---
 doc/doc-docbook/spec.xfpt                        | 153 +++++++++++++++++++++-
 doc/doc-txt/NewStuff                             |   2 +-
 doc/doc-txt/OptionLists.txt                      |   3 +
 src/OS/Makefile-Base                             |   5 +-
 src/scripts/MakeLinks                            |   2 +-
 src/src/EDITME                                   |   1 +
 src/src/auths/Makefile                           |   3 +-
 src/src/auths/external.c                         | 158 +++++++++++++++++++++++
 src/src/auths/external.h                         |  32 +++++
 src/src/config.h.defaults                        |   1 +
 src/src/drtables.c                               |  18 +++
 test/confs/3720                                  |  93 +++++++++++++
 test/confs/3721                                  |   1 +
 test/log/3720                                    |  11 ++
 test/log/3721                                    |  11 ++
 test/scripts/3720-external-auth-GnuTLS/3720      |  38 ++++++
 test/scripts/3720-external-auth-GnuTLS/REQUIRES  |   2 +
 test/scripts/3721-external-auth-OpenSSL/3721     |  38 ++++++
 test/scripts/3721-external-auth-OpenSSL/REQUIRES |   2 +
 test/stdout/3720                                 |  43 ++++++
 test/stdout/3721                                 |  44 +++++++
 21 files changed, 652 insertions(+), 9 deletions(-)


diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt
index 0358ae9..958e7ca 100644
--- a/doc/doc-docbook/spec.xfpt
+++ b/doc/doc-docbook/spec.xfpt
@@ -26128,6 +26128,7 @@ included by setting
AUTH_CRAM_MD5=yes
AUTH_CYRUS_SASL=yes
AUTH_DOVECOT=yes
+AUTH_EXTERNAL=yes
AUTH_GSASL=yes
AUTH_HEIMDAL_GSSAPI=yes
AUTH_PLAINTEXT=yes
@@ -26139,15 +26140,20 @@ authentication mechanism (RFC 2195), and the second provides an interface to
the Cyrus SASL authentication library.
The third is an interface to Dovecot's authentication system, delegating the
work via a socket interface.
-The fourth provides an interface to the GNU SASL authentication library, which
+.new
+The fourth provides for negotiation of authentication done via non-SMTP means,
+as defined by RFC 4422 Appendix A.
+.wen
+The fifth provides an interface to the GNU SASL authentication library, which
provides mechanisms but typically not data sources.
-The fifth provides direct access to Heimdal GSSAPI, geared for Kerberos, but
+The sixth provides direct access to Heimdal GSSAPI, geared for Kerberos, but
supporting setting a server keytab.
-The sixth can be configured to support
+The seventh can be configured to support
the PLAIN authentication mechanism (RFC 2595) or the LOGIN mechanism, which is
-not formally documented, but used by several MUAs. The seventh authenticator
+not formally documented, but used by several MUAs.
+The eighth authenticator
supports Microsoft's &'Secure Password Authentication'& mechanism.
-The eighth is an Exim authenticator but not an SMTP one;
+The last is an Exim authenticator but not an SMTP one;
instead it can use information from a TLS negotiation.

The authenticators are configured using the same syntax as other drivers (see
@@ -27385,6 +27391,143 @@ msn:
. ////////////////////////////////////////////////////////////////////////////
. ////////////////////////////////////////////////////////////////////////////

+.chapter "The external authenticator" "CHAPexternauth"
+.scindex IIDexternauth1 "&(external)& authenticator"
+.scindex IIDexternauth2 "authenticators" "&(external)&"
+.cindex "authentication" "Client Certificate"
+.cindex "authentication" "X509"
+.cindex "Certificate-based authentication"
+The &(external)& authenticator provides support for
+authentication based on non-SMTP information.
+The specification is in RFC 4422 Appendix A
+(&url(https://tools.ietf.org/html/rfc4422)).
+It is only a transport and negotiation mechanism;
+the process of authentication is entirely controlled
+by the server configuration.
+
+The client presents an identity in-clear.
+It is probably wise for a server to only advertise,
+and for clients to only attempt,
+this authentication method on a secure (eg. under TLS) connection.
+
+One possible use, compatible with the
+K-9 Mail Andoid client (&url(https://k9mail.github.io/)),
+is for using X509 client certificates.
+
+It thus overlaps in function with the TLS authenticator
+(see &<<CHAPtlsauth>>&)
+but is a full SMTP SASL authenticator
+rather than being implicit for TLS-connection carried
+client certificates only.
+
+The examples and discussion in this chapter assume that 
+client-certificate authentication is being done.
+
+The client must present a certificate,
+for which it must have been requested via the
+&%tls_verify_hosts%& or &%tls_try_verify_hosts%& main options
+(see &<<CHAPTLS>>&).
+For authentication to be effective the certificate should be
+verifiable against a trust-anchor certificate known to the server.
+
+.section "External options" "SECTexternsoptions"
+.cindex "options" "&(external)& authenticator (server)"
+The &(external)& authenticator has two server options:
+
+.option server_param2 external string&!! unset
+.option server_param3 external string&!! unset
+.cindex "variables (&$auth1$& &$auth2$& etc)" "in &(external)& authenticator"
+These options are expanded before the &%server_condition%& option
+and the result are placed in &$auth2$& and &$auth3$& resectively.
+If the expansion is forced to fail, authentication fails. Any other expansion
+failure causes a temporary error code to be returned.
+
+They can be used to clarify the coding of a complex &%server_condition%&.
+
+.section "Using external in a server" "SECTexternserver"
+.cindex "AUTH" "in &(external)& authenticator"
+.cindex "numerical variables (&$1$& &$2$& etc)" &&&
+        "in &(external)& authenticator"
+.vindex "&$auth1$&, &$auth2$&, etc"
+.cindex "base64 encoding" "in &(external)& authenticator"
+
+When running as a server, &(external)& performs the authentication test by
+expanding a string. The data sent by the client with the AUTH command, or in
+response to subsequent prompts, is base64 encoded, and so may contain any byte
+values when decoded. The decoded value is treated as
+an identity for authentication and
+placed in the expansion variable &$auth1$&.
+
+For compatibility with previous releases of Exim, the value is also placed in
+the expansion variable &$1$&. However, the use of this
+variable for this purpose is now deprecated, as it can lead to confusion in
+string expansions that also use them for other things.
+
+.vindex "&$authenticated_id$&"
+Once an identity has been received,
+&%server_condition%& is expanded. If the expansion is forced to fail,
+authentication fails. Any other expansion failure causes a temporary error code
+to be returned. If the result of a successful expansion is an empty string,
+&"0"&, &"no"&, or &"false"&, authentication fails. If the result of the
+expansion is &"1"&, &"yes"&, or &"true"&, authentication succeeds and the
+generic &%server_set_id%& option is expanded and saved in &$authenticated_id$&.
+For any other result, a temporary error code is returned, with the expanded
+string as the error text.
+
+Example:
+.code
+ext_ccert_san_mail:
+  driver =            external
+  public_name =       EXTERNAL
+
+  server_advertise_condition = $tls_in_certificate_verified
+  server_param2 =     ${certextract {subj_altname,mail,>:} \
+                                    {$tls_in_peercert}}
+  server_condition =  ${if forany {$auth2} \
+                            {eq {$item}{$auth1}}}
+  server_set_id =     $auth1
+.endd
+This accepts a client certificate that is verifiable against any
+of your configured trust-anchors
+(which usually means the full set of public CAs)
+and which has a mail-SAN matching the claimed identity sent by the client.
+
+Note that, up to TLS1.2, the client cert is on the wire in-clear, including the SAN,
+The account name is therefore guessable by an opponent.
+TLS 1.3 protects both server and client certificates, and is not vulnerable
+in this way.
+Likewise, a traditional plaintext SMTP AUTH done inside TLS is not.
+
+
+.section "Using external in a client" "SECTexternclient"
+.cindex "options" "&(external)& authenticator (client)"
+The &(external)& authenticator has one client option:
+
+.option client_send external string&!! unset
+This option is expanded and sent with the AUTH command as the
+identity being asserted.
+
+Example:
+.code
+ext_ccert:
+  driver =      external
+  public_name = EXTERNAL
+
+  client_condition = ${if !eq{$tls_out_cipher}{}}
+  client_send = myaccount@???
+.endd
+
+
+.ecindex IIDexternauth1
+.ecindex IIDexternauth2
+
+
+
+
+
+. ////////////////////////////////////////////////////////////////////////////
+. ////////////////////////////////////////////////////////////////////////////
+
 .chapter "The tls authenticator" "CHAPtlsauth"
 .scindex IIDtlsauth1 "&(tls)& authenticator"
 .scindex IIDtlsauth2 "authenticators" "&(tls)&"
diff --git a/doc/doc-txt/NewStuff b/doc/doc-txt/NewStuff
index a3c736d..07577ec 100644
--- a/doc/doc-txt/NewStuff
+++ b/doc/doc-txt/NewStuff
@@ -9,7 +9,7 @@ the documentation is updated, this file is reduced to a short list.
 Version 4.93
 ------------


-    (none yet)
+ 1. An "external" authenticator, per RFC 4422 Appendix A.



 Version 4.92
diff --git a/doc/doc-txt/OptionLists.txt b/doc/doc-txt/OptionLists.txt
index 0ef35f5..069a787 100644
--- a/doc/doc-txt/OptionLists.txt
+++ b/doc/doc-txt/OptionLists.txt
@@ -132,6 +132,7 @@ client_ignore_invalid_base64         boolean         false         plaintext
 client_name                          string*         +             cram_md5          3.10
 client_secret                        string*         unset         cram_md5          3.10
 client_send                          string*         unset         plaintext         3.10
+client_send                          string*         unset         external (auth)   4.93
 command                              string*         unset         lmtp              3.20
                                                      unset         pipe
                                                      unset         queryprogram      4.00
@@ -501,6 +502,8 @@ server_password                      string          unset         gsasl
 server_param1                 string*         unset       tls (auth)         4.86
 server_param2                 string*         unset       tls (auth)         4.86
 server_param3                 string*         unset       tls (auth)         4.86
+server_param2                 string*         unset       tls (auth)         4.86 (tls-only) 4.93 (external)
+server_param3                 string*         unset       tls (auth)         4.86 (tls-only) 4.93 (external)
 server_prompts                       string*         unset         plaintext         3.10
 server_realm                         string          unset         cyrus_sasl,gsasl  4.43 (cyrus-only) 4.80 (others)
 server_scram_iter                    string*         unset         gsasl             4.80
diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index fed3134..79bec06 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -135,7 +135,7 @@ OBJ_MACRO = macro_predef.o \
     macro-smtp.o macro-accept.o macro-dnslookup.o macro-ipliteral.o macro-iplookup.o \
     macro-manualroute.o macro-queryprogram.o macro-redirect.o \
     macro-auth-spa.o macro-cram_md5.o macro-cyrus_sasl.o macro-dovecot.o macro-gsasl_exim.o \
-    macro-heimdal_gssapi.o macro-plaintext.o macro-spa.o macro-authtls.o \
+    macro-heimdal_gssapi.o macro-plaintext.o macro-spa.o macro-authtls.o macro-external.o \
     macro-dkim.o macro-malware.o macro-signing.o


 $(OBJ_MACRO):    $(MACRO_HSRC)
@@ -212,6 +212,9 @@ macro-cyrus_sasl.o :    auths/cyrus_sasl.c
 macro-dovecot.o:    auths/dovecot.c
     @echo "$(CC) -DMACRO_PREDEF auths/dovecot.c"
     $(FE)$(CC) -c $(CFLAGS) -DMACRO_PREDEF $(INCLUDE) -o $@ auths/dovecot.c
+macro-external.o:    auths/external.c
+    @echo "$(CC) -DMACRO_PREDEF auths/external.c"
+    $(FE)$(CC) -c $(CFLAGS) -DMACRO_PREDEF $(INCLUDE) -o $@ auths/external.c
 macro-gsasl_exim.o :    auths/gsasl_exim.c
     @echo "$(CC) -DMACRO_PREDEF auths/gsasl_exim.c"
     $(FE)$(CC) -c $(CFLAGS) -DMACRO_PREDEF $(INCLUDE) -o $@ auths/gsasl_exim.c
diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks
index 08f4ff1..f69bde4 100755
--- a/src/scripts/MakeLinks
+++ b/src/scripts/MakeLinks
@@ -75,7 +75,7 @@ for f in README Makefile call_pam.c call_pwcheck.c \
   gsasl_exim.h get_data.c get_no64_data.c heimdal_gssapi.c heimdal_gssapi.h \
   md5.c xtextencode.c xtextdecode.c cram_md5.c cram_md5.h plaintext.c plaintext.h \
   pwcheck.c pwcheck.h auth-spa.c auth-spa.h dovecot.c dovecot.h sha1.c spa.c \
-  spa.h tls.c tls.h
+  spa.h tls.c tls.h external.c external.h
 do
   ln -s ../../src/auths/$f $f
 done
diff --git a/src/src/EDITME b/src/src/EDITME
index cbb0805..15360db 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -643,6 +643,7 @@ FIXED_NEVER_USERS=root
 # AUTH_CRAM_MD5=yes
 # AUTH_CYRUS_SASL=yes
 # AUTH_DOVECOT=yes
+# AUTH_EXTERNAL=yes
 # AUTH_GSASL=yes
 # AUTH_GSASL_PC=libgsasl
 # AUTH_HEIMDAL_GSSAPI=yes
diff --git a/src/src/auths/Makefile b/src/src/auths/Makefile
index 62ce9d0..402f141 100644
--- a/src/src/auths/Makefile
+++ b/src/src/auths/Makefile
@@ -7,7 +7,7 @@


 OBJ = auth-spa.o call_pam.o call_pwcheck.o \
       call_radius.o check_serv_cond.o cram_md5.o cyrus_sasl.o dovecot.o \
-      get_data.o get_no64_data.o gsasl_exim.o heimdal_gssapi.o \
+      external.o get_data.o get_no64_data.o gsasl_exim.o heimdal_gssapi.o \
       md5.o plaintext.o pwcheck.o \
       spa.o tls.o xtextdecode.o xtextencode.o


@@ -36,6 +36,7 @@ xtextencode.o:      $(HDRS) xtextencode.c
 cram_md5.o:         $(HDRS) cram_md5.c cram_md5.h
 cyrus_sasl.o:       $(HDRS) cyrus_sasl.c cyrus_sasl.h
 dovecot.o:          $(HDRS) dovecot.c dovecot.h
+external.o:         $(HDRS) external.c external.h
 gsasl_exim.o:       $(HDRS) gsasl_exim.c gsasl_exim.h
 heimdal_gssapi.o:   $(HDRS) heimdal_gssapi.c heimdal_gssapi.h
 plaintext.o:        $(HDRS) plaintext.c plaintext.h
diff --git a/src/src/auths/external.c b/src/src/auths/external.c
new file mode 100644
index 0000000..10e1366
--- /dev/null
+++ b/src/src/auths/external.c
@@ -0,0 +1,158 @@
+/*************************************************
+*     Exim - an Internet mail transport agent    *
+*************************************************/
+
+/* Copyright (c) Jeremy Harris 2019 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+/* This file provides an Exim authenticator driver for
+a server to verify a client SSL certificate, using the EXTERNAL
+method defined in RFC 4422 Appendix A.
+*/
+
+
+#include "../exim.h"
+#include "external.h"
+
+/* Options specific to the external authentication mechanism. */
+
+optionlist auth_external_options[] = {
+  { "client_send",    opt_stringptr,
+      (void *)(offsetof(auth_external_options_block, client_send)) },
+  { "server_param2",    opt_stringptr,
+      (void *)(offsetof(auth_external_options_block, server_param2)) },
+  { "server_param3",        opt_stringptr,
+      (void *)(offsetof(auth_external_options_block, server_param3)) },
+};
+
+/* Size of the options list. An extern variable has to be used so that its
+address can appear in the tables drtables.c. */
+
+int auth_external_options_count = nelem(auth_external_options);
+
+/* Default private options block for the authentication method. */
+
+auth_external_options_block auth_external_option_defaults = {
+    .server_param2 = NULL,
+    .server_param3 = NULL,
+
+    .client_send = NULL,
+};
+
+
+#ifdef MACRO_PREDEF
+
+/* Dummy values */
+void auth_external_init(auth_instance *ablock) {}
+int auth_external_server(auth_instance *ablock, uschar *data) {return 0;}
+int auth_external_client(auth_instance *ablock, void * sx,
+  int timeout, uschar *buffer, int buffsize) {return 0;}
+
+#else   /*!MACRO_PREDEF*/
+
+
+
+
+/*************************************************
+*          Initialization entry point            *
+*************************************************/
+
+/* Called for each instance, after its options have been read, to
+enable consistency checks to be done, or anything else that needs
+to be set up. */
+
+void
+auth_external_init(auth_instance *ablock)
+{
+auth_external_options_block * ob = (auth_external_options_block *)ablock->options_block;
+if (!ablock->public_name) ablock->public_name = ablock->name;
+if (ablock->server_condition) ablock->server = TRUE;
+if (ob->client_send) ablock->client = TRUE;
+}
+
+
+
+/*************************************************
+*             Server entry point                 *
+*************************************************/
+
+/* For interface, see auths/README */
+
+int
+auth_external_server(auth_instance * ablock, uschar * data)
+{
+auth_external_options_block * ob = (auth_external_options_block *)ablock->options_block;
+int rc;
+
+/* If data was supplied on the AUTH command, decode it, and split it up into
+multiple items at binary zeros. The strings are put into $auth1, $auth2, etc,
+up to a maximum. To retain backwards compatibility, they are also put int $1,
+$2, etc. If the data consists of the string "=" it indicates a single, empty
+string. */
+
+if (*data)
+  if ((rc = auth_read_input(data)) != OK)
+    return rc;
+
+/* Now go through the list of prompt strings. Skip over any whose data has
+already been provided as part of the AUTH command. For the rest, send them
+out as prompts, and get a data item back. If the data item is "*", abandon the
+authentication attempt. Otherwise, split it into items as above. */
+
+if (expand_nmax == 0)     /* skip if rxd data */
+  if ((rc = auth_prompt(CUS"")) != OK)
+    return rc;
+
+if (ob->server_param2)
+  {
+  uschar * s = expand_string(ob->server_param2);
+  auth_vars[expand_nmax] = s;
+  expand_nstring[++expand_nmax] = s;
+  expand_nlength[expand_nmax] = Ustrlen(s);
+  if (ob->server_param3)
+    {
+    s = expand_string(ob->server_param3);
+    auth_vars[expand_nmax] = s;
+    expand_nstring[++expand_nmax] = s;
+    expand_nlength[expand_nmax] = Ustrlen(s);
+    }
+  }
+
+return auth_check_serv_cond(ablock);
+}
+
+
+
+/*************************************************
+*              Client entry point                *
+*************************************************/
+
+/* For interface, see auths/README */
+
+int
+auth_external_client(
+  auth_instance *ablock,                 /* authenticator block */
+  void * sx,                 /* smtp connextion */
+  int timeout,                           /* command timeout */
+  uschar *buffer,                        /* buffer for reading response */
+  int buffsize)                          /* size of buffer */
+{
+auth_external_options_block *ob =
+  (auth_external_options_block *)(ablock->options_block);
+const uschar * text = ob->client_send;
+int rc;
+
+/* We output an AUTH command with one expanded argument, the client_send option */
+
+if ((rc = auth_client_item(sx, ablock, &text, AUTH_ITEM_FIRST | AUTH_ITEM_LAST,
+      timeout, buffer, buffsize)) != OK)
+  return rc == DEFER ? FAIL : rc;
+
+if (text) auth_vars[0] = string_copy(text);
+return OK;
+}
+
+
+
+#endif   /*!MACRO_PREDEF*/
+/* End of external.c */
diff --git a/src/src/auths/external.h b/src/src/auths/external.h
new file mode 100644
index 0000000..7d43650
--- /dev/null
+++ b/src/src/auths/external.h
@@ -0,0 +1,32 @@
+/*************************************************
+*     Exim - an Internet mail transport agent    *
+*************************************************/
+
+/* Copyright (c) Jeremy Harris 2019 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+/* Private structure for the private options. */
+
+typedef struct {
+  uschar * server_param2;
+  uschar * server_param3;
+
+  uschar * client_send;
+} auth_external_options_block;
+
+/* Data for reading the private options. */
+
+extern optionlist auth_external_options[];
+extern int auth_external_options_count;
+
+/* Block containing default values. */
+
+extern auth_external_options_block auth_external_option_defaults;
+
+/* The entry points for the mechanism */
+
+extern void auth_external_init(auth_instance *);
+extern int auth_external_server(auth_instance *, uschar *);
+extern int auth_external_client(auth_instance *, void *, int, uschar *, int);
+
+/* End of external.h */
diff --git a/src/src/config.h.defaults b/src/src/config.h.defaults
index 7c2e534..74ec3f4 100644
--- a/src/src/config.h.defaults
+++ b/src/src/config.h.defaults
@@ -24,6 +24,7 @@ Do not put spaces between # and the 'define'.
 #define AUTH_CRAM_MD5
 #define AUTH_CYRUS_SASL
 #define AUTH_DOVECOT
+#define AUTH_EXTERNAL
 #define AUTH_GSASL
 #define AUTH_HEIMDAL_GSSAPI
 #define AUTH_PLAINTEXT
diff --git a/src/src/drtables.c b/src/src/drtables.c
index cd12dd1..54d03ed 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -35,6 +35,10 @@ set to NULL for those that are not compiled into the binary. */
 #include "auths/dovecot.h"
 #endif


+#ifdef AUTH_EXTERNAL
+#include "auths/external.h"
+#endif
+
#ifdef AUTH_GSASL
#include "auths/gsasl_exim.h"
#endif
@@ -101,6 +105,20 @@ auth_info auths_available[] = {
},
#endif

+#ifdef AUTH_EXTERNAL
+  {
+  .driver_name =    US"external",
+  .options =        auth_external_options,
+  .options_count =    &auth_external_options_count,
+  .options_block =    &auth_external_option_defaults,
+  .options_len =    sizeof(auth_external_options_block),
+  .init =        auth_external_init,
+  .servercode =        auth_external_server,
+  .clientcode =        auth_external_client,
+  .version_report =    NULL
+  },
+#endif
+
 #ifdef AUTH_GSASL
   {
   .driver_name =    US"gsasl",
diff --git a/test/confs/3720 b/test/confs/3720
new file mode 100644
index 0000000..6d8c467
--- /dev/null
+++ b/test/confs/3720
@@ -0,0 +1,93 @@
+# Exim test configuration 3720
+
+SERVER=
+
+.include DIR/aux-var/tls_conf_prefix
+
+primary_hostname = myhost.test.ex
+log_selector = +received_recipients +outgoing_port
+
+# ----- Main settings -----
+
+acl_smtp_auth = log_call
+acl_smtp_mail = check_authd
+acl_smtp_rcpt = check_authd
+acl_smtp_data = ar_header
+
+queue_only
+queue_run_in_order
+trusted_users = CALLER
+
+tls_advertise_hosts = *
+tls_certificate = DIR/aux-fixed/cert1
+
+tls_verify_hosts = *
+tls_verify_certificates = DIR/aux-fixed/cert2
+
+
+# ----- ACL -----
+
+begin acl
+
+log_call:
+  accept   logwrite = Auth ACL called, after smtp cmd "$smtp_command"
+
+check_authd:
+  deny     message = authentication required
+          !authenticated = *
+  accept
+
+ar_header:
+  accept  add_header = :at_start:${authresults {$primary_hostname}}
+
+# ----- Authentication -----
+
+begin authenticators
+
+ext_ccert_cn:
+  driver =        external
+  public_name =        EXTERNAL
+
+  server_advertise_condition = ${if eq{$tls_in_cipher}{}{no}{yes}}
+  server_param2 =    ${certextract {subject,CN} {$tls_in_peercert}}
+  server_condition =    ${if eq {$auth2}{$auth1}}
+  server_set_id =    $auth1
+  server_debug_print =    +++TLS \$auth1="$auth1"
+
+  client_send =        "Phil Pennock"
+
+
+# ----- Routers -----
+
+begin routers
+
+server_r:
+  driver =    accept
+  condition =    ${if eq {server}{SERVER}}
+  transport =    file
+
+client_r1:
+  driver =    accept
+  transport =    t1
+
+
+# ----- Transports -----
+
+begin transports
+
+t1:
+  driver = smtp
+  hosts = 127.0.0.1
+  port = PORT_D
+  allow_localhost
+  tls_certificate =        DIR/aux-fixed/cert2
+  tls_verify_certificates =    DIR/aux-fixed/cert1
+  tls_verify_cert_hostnames =    :
+  hosts_try_auth =        *
+
+file:
+  driver = appendfile
+  file = DIR/test-mail/$local_part
+  user = CALLER
+
+# End
diff --git a/test/confs/3721 b/test/confs/3721
new file mode 120000
index 0000000..e401101
--- /dev/null
+++ b/test/confs/3721
@@ -0,0 +1 @@
+3720
\ No newline at end of file
diff --git a/test/log/3720 b/test/log/3720
new file mode 100644
index 0000000..f79e756
--- /dev/null
+++ b/test/log/3720
@@ -0,0 +1,11 @@
+1999-03-02 09:44:33 10HmaX-0005vi-00 <= ok@??? U=CALLER P=local S=sss for x@y
+1999-03-02 09:44:33 Start queue run: pid=pppp
+1999-03-02 09:44:33 10HmaX-0005vi-00 => x@y R=client_r1 T=t1 H=127.0.0.1 [127.0.0.1]:1225 X=TLS_proto_and_cipher CV=yes A=ext_ccert_cn C="250 OK id=10HmaY-0005vi-00"
+1999-03-02 09:44:33 10HmaX-0005vi-00 Completed
+1999-03-02 09:44:33 End queue run: pid=pppp
+
+******** SERVER ********
+1999-03-02 09:44:33 exim x.yz daemon started: pid=pppp, no queue runs, listening for SMTP on port 1225
+1999-03-02 09:44:33 Auth ACL called, after smtp cmd "AUTH EXTERNAL UGhpbCBQZW5ub2Nr"
+1999-03-02 09:44:33 Auth ACL called, after smtp cmd "AUTH EXTERNAL UGhpbCBQZW5ub2Nr"
+1999-03-02 09:44:33 10HmaY-0005vi-00 <= ok@??? H=localhost (myhost.test.ex) [127.0.0.1] P=esmtpsa X=TLS_proto_and_cipher CV=yes A=ext_ccert_cn:Phil Pennock S=sss id=E10HmaX-0005vi-00@??? for x@y
diff --git a/test/log/3721 b/test/log/3721
new file mode 100644
index 0000000..f79e756
--- /dev/null
+++ b/test/log/3721
@@ -0,0 +1,11 @@
+1999-03-02 09:44:33 10HmaX-0005vi-00 <= ok@??? U=CALLER P=local S=sss for x@y
+1999-03-02 09:44:33 Start queue run: pid=pppp
+1999-03-02 09:44:33 10HmaX-0005vi-00 => x@y R=client_r1 T=t1 H=127.0.0.1 [127.0.0.1]:1225 X=TLS_proto_and_cipher CV=yes A=ext_ccert_cn C="250 OK id=10HmaY-0005vi-00"
+1999-03-02 09:44:33 10HmaX-0005vi-00 Completed
+1999-03-02 09:44:33 End queue run: pid=pppp
+
+******** SERVER ********
+1999-03-02 09:44:33 exim x.yz daemon started: pid=pppp, no queue runs, listening for SMTP on port 1225
+1999-03-02 09:44:33 Auth ACL called, after smtp cmd "AUTH EXTERNAL UGhpbCBQZW5ub2Nr"
+1999-03-02 09:44:33 Auth ACL called, after smtp cmd "AUTH EXTERNAL UGhpbCBQZW5ub2Nr"
+1999-03-02 09:44:33 10HmaY-0005vi-00 <= ok@??? H=localhost (myhost.test.ex) [127.0.0.1] P=esmtpsa X=TLS_proto_and_cipher CV=yes A=ext_ccert_cn:Phil Pennock S=sss id=E10HmaX-0005vi-00@??? for x@y
diff --git a/test/scripts/3720-external-auth-GnuTLS/3720 b/test/scripts/3720-external-auth-GnuTLS/3720
new file mode 100644
index 0000000..49d9520
--- /dev/null
+++ b/test/scripts/3720-external-auth-GnuTLS/3720
@@ -0,0 +1,38 @@
+# External authentication (server & client)
+munge tls_anycipher
+#
+exim -DSERVER=server -bd -oX PORT_D
+****
+#
+#
+client-gnutls 127.0.0.1 PORT_D 127.0.0.1 DIR/aux-fixed/cert2 DIR/aux-fixed/cert2
+??? 220
+EHLO tester
+??? 250-
+??? 250-
+??? 250-
+??? 250-
+??? 250-STARTTLS
+??? 250 HELP
+STARTTLS
+??? 220
+EHLO tester
+??? 250-
+??? 250-
+??? 250-
+??? 250-
+??? 250-AUTH EXTERNAL
+??? 250 HELP
+AUTH EXTERNAL UGhpbCBQZW5ub2Nr
+??? 235
+quit
+??? 221
+****
+#
+exim -f ok@??? x@y
+****
+exim -q
+****
+#
+killdaemon
+no_msglog_check
diff --git a/test/scripts/3720-external-auth-GnuTLS/REQUIRES b/test/scripts/3720-external-auth-GnuTLS/REQUIRES
new file mode 100644
index 0000000..9e358e2
--- /dev/null
+++ b/test/scripts/3720-external-auth-GnuTLS/REQUIRES
@@ -0,0 +1,2 @@
+authenticator external
+support GnuTLS
diff --git a/test/scripts/3721-external-auth-OpenSSL/3721 b/test/scripts/3721-external-auth-OpenSSL/3721
new file mode 100644
index 0000000..310b8d2
--- /dev/null
+++ b/test/scripts/3721-external-auth-OpenSSL/3721
@@ -0,0 +1,38 @@
+# External authentication (server & client)
+munge tls_anycipher
+#
+exim -DSERVER=server -bd -oX PORT_D
+****
+#
+#
+client-ssl 127.0.0.1 PORT_D 127.0.0.1 DIR/aux-fixed/cert2 DIR/aux-fixed/cert2
+??? 220
+EHLO tester
+??? 250-
+??? 250-
+??? 250-
+??? 250-
+??? 250-STARTTLS
+??? 250 HELP
+STARTTLS
+??? 220
+EHLO tester
+??? 250-
+??? 250-
+??? 250-
+??? 250-
+??? 250-AUTH EXTERNAL
+??? 250 HELP
+AUTH EXTERNAL UGhpbCBQZW5ub2Nr
+??? 235
+quit
+??? 221
+****
+#
+exim -f ok@??? x@y
+****
+exim -q
+****
+#
+killdaemon
+no_msglog_check
diff --git a/test/scripts/3721-external-auth-OpenSSL/REQUIRES b/test/scripts/3721-external-auth-OpenSSL/REQUIRES
new file mode 100644
index 0000000..c0a56a2
--- /dev/null
+++ b/test/scripts/3721-external-auth-OpenSSL/REQUIRES
@@ -0,0 +1,2 @@
+authenticator external
+support OpenSSL
diff --git a/test/stdout/3720 b/test/stdout/3720
new file mode 100644
index 0000000..049c87d
--- /dev/null
+++ b/test/stdout/3720
@@ -0,0 +1,43 @@
+Connecting to 127.0.0.1 port 1225 ... connected
+Certificate file = TESTSUITE/aux-fixed/cert2
+Key file = TESTSUITE/aux-fixed/cert2
+??? 220
+<<< 220 myhost.test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000
+>>> EHLO tester
+??? 250-
+<<< 250-myhost.test.ex Hello tester [127.0.0.1]
+??? 250-
+<<< 250-SIZE 52428800
+??? 250-
+<<< 250-8BITMIME
+??? 250-
+<<< 250-PIPELINING
+??? 250-STARTTLS
+<<< 250-STARTTLS
+??? 250 HELP
+<<< 250 HELP
+>>> STARTTLS
+??? 220
+<<< 220 TLS go ahead
+Attempting to start TLS
+Succeeded in starting TLS
+>>> EHLO tester
+??? 250-
+<<< 250-myhost.test.ex Hello tester [127.0.0.1]
+??? 250-
+<<< 250-SIZE 52428800
+??? 250-
+<<< 250-8BITMIME
+??? 250-
+<<< 250-PIPELINING
+??? 250-AUTH EXTERNAL
+<<< 250-AUTH EXTERNAL
+??? 250 HELP
+<<< 250 HELP
+>>> AUTH EXTERNAL UGhpbCBQZW5ub2Nr
+??? 235
+<<< 235 Authentication succeeded
+>>> quit
+??? 221
+<<< 221 myhost.test.ex closing connection
+End of script
diff --git a/test/stdout/3721 b/test/stdout/3721
new file mode 100644
index 0000000..81878f9
--- /dev/null
+++ b/test/stdout/3721
@@ -0,0 +1,44 @@
+Connecting to 127.0.0.1 port 1225 ... connected
+Certificate file = TESTSUITE/aux-fixed/cert2
+Key file = TESTSUITE/aux-fixed/cert2
+??? 220
+<<< 220 myhost.test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000
+>>> EHLO tester
+??? 250-
+<<< 250-myhost.test.ex Hello tester [127.0.0.1]
+??? 250-
+<<< 250-SIZE 52428800
+??? 250-
+<<< 250-8BITMIME
+??? 250-
+<<< 250-PIPELINING
+??? 250-STARTTLS
+<<< 250-STARTTLS
+??? 250 HELP
+<<< 250 HELP
+>>> STARTTLS
+??? 220
+<<< 220 TLS go ahead
+Attempting to start TLS
+SSL connection using ke-RSA-AES256-SHA
+Succeeded in starting TLS
+>>> EHLO tester
+??? 250-
+<<< 250-myhost.test.ex Hello tester [127.0.0.1]
+??? 250-
+<<< 250-SIZE 52428800
+??? 250-
+<<< 250-8BITMIME
+??? 250-
+<<< 250-PIPELINING
+??? 250-AUTH EXTERNAL
+<<< 250-AUTH EXTERNAL
+??? 250 HELP
+<<< 250 HELP
+>>> AUTH EXTERNAL UGhpbCBQZW5ub2Nr
+??? 235
+<<< 235 Authentication succeeded
+>>> quit
+??? 221
+<<< 221 myhost.test.ex closing connection
+End of script