Search code examples
c#asp.net-corex509certificatemailkitmimekit

C# How to S/Mime in Asp.net 5 with Bouncy Castle instead of x509certificate2


I successfully used the below code to send an S/Mime e-mail on latest Windows Server, but it fails on 2012 Windows Server. I think 2012 does not support newer AES encryption.

How would I rewrite the following code to replace System.Security.Cryptography.X509Certificates.X509Certificate2() with Bouncy Castle, also using MimeKit and Mailkit? I tried using older DES3 for 2012, but it fails with CryptoThrowHelper+WindowsCryptographicException: The specified network password is not correct. Exactly the same code runs just fine on Windows 10 on my developer machine. Ultimately I want this to run in a Linux container, plus I think Bouncy Castle is much more fun to type and say. :)

var message = new MimeMessage(); // Using MimeKit, MailKit 
message.To.Add( new MailboxAddress("John Doe","jdoe@somewhere.com"));
message.From.Add( new MailboxAddress("Jane Doe","jndoe@somewhere.com"));
message.Headers.Add( "AS3-From", "PILM");
message.Headers.Add( "AS3-To", "SARS");
message.Date = DateTimeOffset.Now;
message.MessageId = "42";
message.Subject = "This is a subject";
message.Body = new TextPart("html") { Text = "This is a body" };

using (var context = new TemporarySecureMimeContext())
{   // TODO: Replace Microsoft Cryptography with BouncyCastle
    var cert = new System.Security.Cryptography
        .X509Certificates.X509Certificate2(
        @"c:\security\smime.p12", "VeryCoolPassword",
        System.Security.Cryptography.X509Certificates
            .X509KeyStorageFlags.EphemeralKeySet);

    var recip = new CmsRecipient(cert) {
        EncryptionAlgorithms = new EncryptionAlgorithm[] { 
            EncryptionAlgorithm.TripleDes }
    };
    var recips = new CmsRecipientCollection();
    recips.Add(recip);

    message.Body = ApplicationPkcs7Mime.Encrypt(
        context, recips, message.Body );
};

var client = new SmtpClient();
client.Connect("MySuperEmailServer", 465, true);
client.Authenticate("MySuperUserName", "VeryCoolPassword");
client.Send(message);
client.Disconnect(true);
client.Dispose();

Solution

  • There seems to be several parts to this question.

    First, you want to know how to load a S/MIME certificate using BouncyCastle. Since, as Crypt32 pointed out, you aren't likely to need to load *.pfx (aka *.p12) files since you wouldn't have their private key, you'll likely need to load a *.cer (or *.crt) file:

    using (var stream = File.OpenRead ("smime-recipient-certificate.cer")) {
        var parser = new X509CertificateParser ();
    
        var certificate = parser.ReadCertificate (stream);
    }
    

    MimeKit's CmsRecipient class will do this for you if you call the CmsRecipient .ctor that takes a fileName argument.

    That said, it never hurts to know how to load it manually in case you maybe don't have it stored on disk somewhere (e.g. maybe a MemoryStream or something).

    The CmsRecipient .ctor that takes an X509Certificate2 (that you are currently using) is also fine to use. MimeKit just converts the X509Certificate2 certificate into a BouncyCastle certificate internally.

    That said, based on your conversation with Crypt32, it sounds like the issue you were having is with loading an X509Certificate2 from a .pfx file on Windows Server 2012? In which case, loading the certificate using BouncyCastle is probably the way to go.

    If you need or want to know how to load a .pfx file using BouncyCastle, you can do that using the following logic:

    // Note: BouncyCastle's X509Certificate class is named X509Certificate, not to be
    // confused with System.Security.Cryptography.X509Certificates.X509Certificate.
    X509Certificate LoadPfx (Stream stream, string password, out AsymmetricKeyParameter privateKey)
    {
        var pkcs12 = new Pkcs12Store (stream, password.ToCharArray ());
    
        foreach (string alias in pkcs12.Aliases) {
            if (!pkcs12.IsKeyEntry (alias))
                continue;
    
            var chain = pkcs12.GetCertificateChain (alias);
    
            if (chain.Length == 0)
                continue;
    
            var keyEntry = pkcs12.GetKey (alias);
    
            if (!keyEntry.Key.IsPrivate)
                continue;
    
            privateKey = keyEntry.Key;
    
            return chain[0].Certificate;
        }
    
        throw Exception ("Did not find a certificate with a private key.");
    }
    

    Note: I would probably suggest changing this:

    message.MessageId = "42";
    

    to this:

    message.MessageId = MimeUtils.GenerateMessageId();
    

    This will generate a random/unique message-id value using the correct syntax (The Message-Id header isn't a number, it's a xyz@abc.com type value).