Randy
Randy

Reputation: 563

Unable to send emails via Mailkit on an Azure web app

Within our organization we're developing a web app (.NET Core 2.0) which is hosted in an Azure App Service. For our emailing infrastructure, we've installed the latest version of MailKit (version 2.11.1 at the time of writing).

Locally, the process of sending emails works properly and no problems occur, however, after deploying the app to our Azure environment an SslHandshakeException is thrown upon connecting.

MailKit.Security.SslHandshakeException: An error occurred while attempting to establish an SSL or TLS connection.

This usually means that the SSL certificate presented by the server is not trusted by the system for one or more of
the following reasons:

1. The server is using a self-signed certificate which cannot be verified.
2. The local system is missing a Root or Intermediate certificate needed to verify the server's certificate.
3. A Certificate Authority CRL server for one or more of the certificates in the chain is temporarily unavailable.
4. The certificate presented by the server is expired or invalid.
5. The set of SSL/TLS protocols supported by the client and server do not match.

See https://github.com/jstedfast/MailKit/blob/master/FAQ.md#SslHandshakeException for possible solutions.
 ---> System.NotSupportedException: The requested security protocol is not supported.

We're using the following configuration (simplified):

using (var client = new SmtpClient())
{
   client.Connect("smtp.office365.com", 587, SecureSocketOptions.StartTls);
   client.AuthenticationMechanisms.Remove("XOAUTH2");
   client.Authenticate("username", "password");
   client.Send(mimeMessage);
}

We've tried playing around with different configuration values (e.g. other ports) but without success.

What did seem to work, though, is downgrading the MailKit package to version 2.3.1.6. Without any configurational changes, the connection did succeed and we were able to send emails.

Could someone explain why the versions behave differently and what steps we possibly need to take to make our configuration work with the newest version of MailKit?

Thanks in advance!

Upvotes: 1

Views: 1368

Answers (1)

jstedfast
jstedfast

Reputation: 38528

MailKit 2.3.1.6 had a default SSL certificate validation callback that was much more liberal in what it accepted as valid.

Newer versions of MailKit do not (in other words, newer versions of MailKit focus on security rather than "just connect to the damn server, I don't care if the SSL certificates are valid or not"). Instead, MailKit now hard-codes the serial numbers and fingerprints of some of the more common mail servers (such as GMail, Yahoo!Mail, Office365 and some others) to make this "magically" work most of the time for people. However, as you have discovered, sometimes these certificates get renewed and the hard-coded values that MailKit has are no longer up-to-date (just released 2.12.0 which updates them, btw).

The best way to solve this is to set your own ServerCertificateValidationCallback on the SmtpClient:

client.ServerCertificateValidationCallback = MySslCertificateValidationCallback;

To help you debug the issue, your callback method could look something like this:

static bool MySslCertificateValidationCallback (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
    // If there are no errors, then everything went smoothly.
    if (sslPolicyErrors == SslPolicyErrors.None)
        return true;

    // Note: MailKit will always pass the host name string as the `sender` argument.
    var host = (string) sender;

    if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) != 0) {
        // This means that the remote certificate is unavailable. Notify the user and return false.
        Console.WriteLine ("The SSL certificate was not available for {0}", host);
        return false;
    }

    if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) != 0) {
        // This means that the server's SSL certificate did not match the host name that we are trying to connect to.
        var certificate2 = certificate as X509Certificate2;
        var cn = certificate2 != null ? certificate2.GetNameInfo (X509NameType.SimpleName, false) : certificate.Subject;

        Console.WriteLine ("The Common Name for the SSL certificate did not match {0}. Instead, it was {1}.", host, cn);
        return false;
    }

    // The only other errors left are chain errors.
    Console.WriteLine ("The SSL certificate for the server could not be validated for the following reasons:");

    // The first element's certificate will be the server's SSL certificate (and will match the `certificate` argument)
    // while the last element in the chain will typically either be the Root Certificate Authority's certificate -or- it
    // will be a non-authoritative self-signed certificate that the server admin created. 
    foreach (var element in chain.ChainElements) {
        // Each element in the chain will have its own status list. If the status list is empty, it means that the
        // certificate itself did not contain any errors.
        if (element.ChainElementStatus.Length == 0)
            continue;

        Console.WriteLine ("\u2022 {0}", element.Certificate.Subject);
        foreach (var error in element.ChainElementStatus) {
            // `error.StatusInformation` contains a human-readable error string while `error.Status` is the corresponding enum value.
            Console.WriteLine ("\t\u2022 {0}", error.StatusInformation);
        }
    }

    return false;
}

One possible solution to your issue, depending on what the problem is, might be something like this:

static bool MyServerCertificateValidationCallback (object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
    if (sslPolicyErrors == SslPolicyErrors.None)
        return true;

    // Note: The following code casts to an X509Certificate2 because it's easier to get the
    // values for comparison, but it's possible to get them from an X509Certificate as well.
    if (certificate is X509Certificate2 certificate2) {
        var cn = certificate2.GetNameInfo (X509NameType.SimpleName, false);
        var fingerprint = certificate2.Thumbprint;
        var serial = certificate2.SerialNumber;
        var issuer = certificate2.Issuer;

        return cn == "outlook.com" &&
            issuer == "CN=DigiCert Cloud Services CA-1, O=DigiCert Inc, C=US" &&
            serial == "0CCAC32B0EF281026392B8852AB15642" &&
            fingerprint == "CBAA1582F1E49AD1D108193B5D38B966BE4993C6";
            // Expires 1/21/2022 6:59:59 PM
    }

    return false;
}

Upvotes: 3

Related Questions