send-email: add client certificate options

For SMTP servers that do "mutual certificate verification", the mail client is required to present its own TLS certificate as well. This patch adds --smtp-ssl-client-cert and --smtp-ssl-client-key for such servers. The problem of which private key for the certificate is chosen arises when there are private keys in both the certificate and private key file. According to the documentation of IO::Socket::SSL(link supplied), the behaviour(the private key chosen) depends on the format of the certificate. In a nutshell, - PKCS12: the key in the cert always takes the precedence - PEM: if the key file is not given, it will "try" to read one from the cert PEM file Many users may find this discrepancy unintuitive. In terms of client certificate, git-send-email is implemented in a way that what's possible with perl's SSL library is exposed to the user as much as possible. In this instance, the user may choose to use a PEM file that contains both certificate and private key should be at their discretion despite the implications. Link: https://metacpan.org/pod/IO::Socket::SSL#SSL_cert_file-%7C-SSL_cert-%7C-SSL_key_file-%7C-SSL_key Link: https://lore.kernel.org/all/319bf98c-52df-4bf9-b157-e4bc2bf087d6@dev.snart.me/ Signed-off-by: David Timber <dxdt@dev.snart.me> Signed-off-by: Junio C Hamano <gitster@pobox.com>

David Timber committed Mar 2, 2026 at 12:16 UTC a8215a2051411bbaa1ea1464026cb473fbf8f83c
3 files changed +71 -11
Documentation/config/sendemail.adoc
+16
@@ -12,6 +12,22 @@ sendemail.smtpSSLCertPath::
12 Path to ca-certificates (either a directory or a single file).
13 Set it to an empty string to disable certificate verification.
14
15 +sendemail.smtpSSLClientCert::
16 + Path to the client certificate file to present if requested by the
17 + server. This is required when the server is set up to verify client
18 + certificates. If the corresponding private key is not included in the
19 + file, it must be supplied using `sendemail.smtpSSLClientKey` or the
20 + `--smtp-ssl-client-key` option.
21 +
22 +sendemail.smtpSSLClientKey::
23 + Path to the client private key file that corresponds to the client
24 + certificate. To avoid misconfiguration, this configuration must be used
25 + in conjunction with `sendemail.smtpSSLClientKey` or the
26 + `--smtp-ssl-client-cert` option. If the client key is included in the
27 + client certificate, the choice of private key depends on the format of
28 + the certificate. Visit https://metacpan.org/pod/IO::Socket::SSL for more
29 + details.
30 +
31 sendemail.<identity>.*::
32 Identity-specific versions of the `sendemail.*` parameters
33 found below, taking precedence over those when this
Documentation/git-send-email.adoc
+19
@@ -290,6 +290,25 @@ must be used for each option.
290 variable, if set, or the backing SSL library's compiled-in default
291 otherwise (which should be the best choice on most platforms).
292
293 +--smtp-ssl-client-cert <path>::
294 + Path to the client certificate file to present if requested by the
295 + server. This option is required when the server is set up to verify
296 + client certificates. If the corresponding private key is not included in
297 + the file, it must be supplied using the `sendemail.smtpSSLClientKey`
298 + configuration variable or the `--smtp-ssl-client-key` option. Defaults
299 + to the value of the `sendemail.smtpSSLClientCert` configuration
300 + variable, if set.
301 +
302 +--smtp-ssl-client-key <path>::
303 + Path to the client private key file that corresponds to the client
304 + certificate. To avoid misconfiguration, this option must be used in
305 + conjunction with the `sendemail.smtpSSLClientKey` configuration variable
306 + or the `--smtp-ssl-client-cert` option. If the client key is included in
307 + the client certificate, the choice of private key depends on the format
308 + of the certificate. Visit https://metacpan.org/pod/IO::Socket::SSL for
309 + more details. Defaults to the value of the `sendemail.smtpSSLClientKey`
310 + configuration variable, if set.
311 +
312 --smtp-user=<user>::
313 Username for SMTP-AUTH. Default is the value of `sendemail.smtpUser`;
314 if a username is not specified (with `--smtp-user` or `sendemail.smtpUser`),
git-send-email.perl
+36 -11
@@ -66,6 +66,8 @@ git send-email --translate-aliases
66 --smtp-ssl-cert-path <str> * Path to ca-certificates (either directory or file).
67 Pass an empty string to disable certificate
68 verification.
69 + --smtp-ssl-client-cert <str> * Path to the client certificate file
70 + --smtp-ssl-client-key <str> * Path to the private key file for the client certificate
71 --smtp-domain <str> * The domain name sent to HELO/EHLO handshake
72 --smtp-auth <str> * Space-separated list of allowed AUTH mechanisms, or
73 "none" to disable authentication.
@@ -279,6 +281,7 @@ my ($cover_cc, $cover_to);
281 my ($to_cmd, $cc_cmd, $header_cmd);
282 my ($smtp_server, $smtp_server_port, @smtp_server_options);
283 my ($smtp_authuser, $smtp_encryption, $smtp_ssl_cert_path);
284 +my ($smtp_ssl_client_cert, $smtp_ssl_client_key);
285 my ($batch_size, $relogin_delay);
286 my ($identity, $aliasfiletype, @alias_files, $smtp_domain, $smtp_auth);
287 my ($imap_sent_folder);
@@ -350,6 +353,8 @@ my %config_settings = (
353 my %config_path_settings = (
354 "aliasesfile" => \@alias_files,
355 "smtpsslcertpath" => \$smtp_ssl_cert_path,
356 + "smtpsslclientcert" => \$smtp_ssl_client_cert,
357 + "smtpsslclientkey" => \$smtp_ssl_client_key,
358 "mailmap.file" => \$mailmap_file,
359 "mailmap.blob" => \$mailmap_blob,
360 );
@@ -531,6 +536,8 @@ my %options = (
536 "smtp-ssl" => sub { $smtp_encryption = 'ssl' },
537 "smtp-encryption=s" => \$smtp_encryption,
538 "smtp-ssl-cert-path=s" => \$smtp_ssl_cert_path,
539 + "smtp-ssl-client-cert=s" => \$smtp_ssl_client_cert,
540 + "smtp-ssl-client-key=s" => \$smtp_ssl_client_key,
541 "smtp-debug:i" => \$debug_net_smtp,
542 "smtp-domain:s" => \$smtp_domain,
543 "smtp-auth=s" => \$smtp_auth,
@@ -1520,6 +1527,8 @@ sub handle_smtp_error {
1527 }
1528
1529 sub ssl_verify_params {
1530 + my %ret = ();
1531 +
1532 eval {
1533 require IO::Socket::SSL;
1534 IO::Socket::SSL->import(qw/SSL_VERIFY_PEER SSL_VERIFY_NONE/);
@@ -1531,20 +1540,36 @@ sub ssl_verify_params {
1540
1541 if (!defined $smtp_ssl_cert_path) {
1542 # use the OpenSSL defaults
1534 - return (SSL_verify_mode => SSL_VERIFY_PEER());
1543 + $ret{SSL_verify_mode} = SSL_VERIFY_PEER();
1544 + }
1545 + else {
1546 + if ($smtp_ssl_cert_path eq "") {
1547 + $ret{SSL_verify_mode} = SSL_VERIFY_NONE();
1548 + } elsif (-d $smtp_ssl_cert_path) {
1549 + $ret{SSL_verify_mode} = SSL_VERIFY_PEER();
1550 + $ret{SSL_ca_path} = $smtp_ssl_cert_path;
1551 + } elsif (-f $smtp_ssl_cert_path) {
1552 + $ret{SSL_verify_mode} = SSL_VERIFY_PEER();
1553 + $ret{SSL_ca_file} = $smtp_ssl_cert_path;
1554 + } else {
1555 + die sprintf(__("CA path \"%s\" does not exist"), $smtp_ssl_cert_path);
1556 + }
1557 }
1558
1537 - if ($smtp_ssl_cert_path eq "") {
1538 - return (SSL_verify_mode => SSL_VERIFY_NONE());
1539 - } elsif (-d $smtp_ssl_cert_path) {
1540 - return (SSL_verify_mode => SSL_VERIFY_PEER(),
1541 - SSL_ca_path => $smtp_ssl_cert_path);
1542 - } elsif (-f $smtp_ssl_cert_path) {
1543 - return (SSL_verify_mode => SSL_VERIFY_PEER(),
1544 - SSL_ca_file => $smtp_ssl_cert_path);
1545 - } else {
1546 - die sprintf(__("CA path \"%s\" does not exist"), $smtp_ssl_cert_path);
1559 + if (defined $smtp_ssl_client_cert) {
1560 + $ret{SSL_cert_file} = $smtp_ssl_client_cert;
1561 }
1562 + if (defined $smtp_ssl_client_key) {
1563 + if (!defined $smtp_ssl_client_cert) {
1564 + # Accept the client key only when a certificate is given.
1565 + # We die here because this case is a user error.
1566 + die sprintf(__("Only client key \"%s\" specified"),
1567 + $smtp_ssl_client_key);
1568 + }
1569 + $ret{SSL_key_file} = $smtp_ssl_client_key;
1570 + }
1571 +
1572 + return %ret;
1573 }
1574
1575 sub file_name_is_absolute {