jstedfast / MimeKit

A .NET MIME creation and parser library with support for S/MIME, PGP, DKIM, TNEF and Unix mbox spools.
http://www.mimekit.net
MIT License
1.83k stars 371 forks source link

Question: Is there also an abstract class to work with X509Certificate2 certificates? #825

Closed Sicos1977 closed 2 years ago

Sicos1977 commented 2 years ago

I now use the abstract class MimeKit.Cryptography.OpenPgpContext to work with Open PGP keys. This works as expected. I also have an requirement to work with X509Certificate2 certificates? So is there a class simular to what the OpenPgpContext class does?

I did some digging around in the Cryptography name space and found this class MimeKit.Cryptography.BouncyCastleSecureMimeContext, is this the one to work with certificates?

And also probably a very dumb question, how does MimeKit know if an e-mail is encrypted or signed with a certificate instead of for example a OpenPGP key?

Do I need to do a check like this or is there some code in MimeKit that figures it out?

var pkcs7 = message.Body as ApplicationPkcs7Mime;

if (pkcs7 != null && pkcs7.SecureMimeType == SecureMimeType.EnvelopedData) ...
Sicos1977 commented 2 years ago

Also for other people that come here and need to know how to make a (self signed) certificate. See the code below. It took me a while to find and figure out all the code I needed. So I hope this message is helpfull for you.

    public static X509Certificate2 GenerateCertificate(
        int keyLength, 
        int durationYears, 
        string subjectName, 
        string friendlyName, 
        string password, 
        out string privateKey, 
        out string publicKey)
    {
        // Prepare random number generation.
        var cryptoApiRandomGenerator = new CryptoApiRandomGenerator();
        var secureRandom = new SecureRandom(cryptoApiRandomGenerator);

        // Create asymmetric key
        var keyGenerationParameters = new KeyGenerationParameters(secureRandom, keyLength);
        var rsaKeyPairGenerator = new RsaKeyPairGenerator();
        rsaKeyPairGenerator.Init(keyGenerationParameters);
        var asymmetricCipherKeyPair = rsaKeyPairGenerator.GenerateKeyPair();
        var x509V3CertificateGenerator = new X509V3CertificateGenerator();

        // Generate a serial number
        var serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(long.MaxValue), secureRandom);
        x509V3CertificateGenerator.SetSerialNumber(serialNumber);

        // Assign the subject name
        var subjectDn = new X509Name($"CN={subjectName}");
        x509V3CertificateGenerator.SetIssuerDN(subjectDn);
        x509V3CertificateGenerator.SetSubjectDN(subjectDn);

        // Set valid dates
        var notBefore = DateTime.UtcNow.Date;
        var notAfter = notBefore.AddYears(durationYears);
        x509V3CertificateGenerator.SetNotBefore(notBefore);
        x509V3CertificateGenerator.SetNotAfter(notAfter);

        // Set key usage for digital signatures and key encipherment
        var keyUsage = new KeyUsage(KeyUsage.DigitalSignature | KeyUsage.KeyEncipherment);
        x509V3CertificateGenerator.AddExtension(X509Extensions.KeyUsage, true, keyUsage);

        var oids = new List<DerObjectIdentifier>
        {
            new("1.3.6.1.5.5.7.3.4"),       // Secure Email
            new("1.3.6.1.4.1.6449.1.3.5.2") // Email Protection
        };

        // Load the OIDs passed in and specify enhanced key usages
        x509V3CertificateGenerator.AddExtension(X509Extensions.ExtendedKeyUsage, true, new ExtendedKeyUsage(oids));

        // Assign the public key
        x509V3CertificateGenerator.SetPublicKey(asymmetricCipherKeyPair.Public);

        // Self-sign the certificate using SHA-512.
        ISignatureFactory signatureFactory = new Asn1SignatureFactory("SHA512WITHRSA", asymmetricCipherKeyPair.Private, secureRandom);
        var bouncyCastleCertificate = x509V3CertificateGenerator.Generate(signatureFactory);

        // Convert from BouncyCastle private key format to System.Security.Cryptography format.
        var x509Certificate = new X509Certificate2(DotNetUtilities.ToX509Certificate(bouncyCastleCertificate))
        {
            PrivateKey = ToDotNetKey((RsaPrivateCrtKeyParameters)asymmetricCipherKeyPair.Private),
            FriendlyName = friendlyName
        };

        using var privateTextWriter = new StringWriter();
        var privatePemWriter = new PemWriter(privateTextWriter);
        if (!string.IsNullOrEmpty(password))
            privatePemWriter.WriteObject(asymmetricCipherKeyPair.Private, "AES-128", password.ToCharArray(), new SecureRandom());
        else
            privatePemWriter.WriteObject(asymmetricCipherKeyPair.Private);

        privatePemWriter.Writer.Flush();
        privateKey = privatePemWriter.ToString();

        using var publicTextWriter = new StringWriter();
        var publicPemWriter = new PemWriter(publicTextWriter);
        publicPemWriter.WriteObject(asymmetricCipherKeyPair.Public);
        publicKey = publicTextWriter.ToString();

        return x509Certificate;
    }

    /// <summary>
    ///     Convert from BouncyCastle private key format to System.Security.Cryptography format.
    /// </summary>
    /// <param name="privateKey">BouncyCastle private key.</param>
    /// <returns>System.Security.Cryptography representation of the key.</returns>
    private static AsymmetricAlgorithm ToDotNetKey(RsaPrivateCrtKeyParameters privateKey)
    {
        var cspParams = new CspParameters
        {
            KeyContainerName = Guid.NewGuid().ToString(),
            KeyNumber = (int)KeyNumber.Exchange,
            Flags = CspProviderFlags.UseMachineKeyStore
        };

        var rsaProvider = new RSACryptoServiceProvider(cspParams);
        var parameters = new RSAParameters
        {
            Modulus = privateKey.Modulus.ToByteArrayUnsigned(),
            P = privateKey.P.ToByteArrayUnsigned(),
            Q = privateKey.Q.ToByteArrayUnsigned(),
            DP = privateKey.DP.ToByteArrayUnsigned(),
            DQ = privateKey.DQ.ToByteArrayUnsigned(),
            InverseQ = privateKey.QInv.ToByteArrayUnsigned(),
            D = privateKey.Exponent.ToByteArrayUnsigned(),
            Exponent = privateKey.PublicExponent.ToByteArrayUnsigned()
        };

        rsaProvider.ImportParameters(parameters);
        return rsaProvider;
    }
Sicos1977 commented 2 years ago

And also some code to generate Open PGP keys

    internal static void GeneratePgpKey(int keyLength, string identifier, string password, out string privateKey, out string publicKey)
    {
        // Genereer een nieuwe RSA key pair 
        var rsaKeyPairGenerator = new RsaKeyPairGenerator();
        rsaKeyPairGenerator.Init(new RsaKeyGenerationParameters(BigInteger.ValueOf(0x101), new SecureRandom(), keyLength, 80));
        var asymmetricCipherKeyPair = rsaKeyPairGenerator.GenerateKeyPair();

        // Maak PGP sub packet
        var hashedGen = new PgpSignatureSubpacketGenerator();
        hashedGen.SetKeyFlags(true, PgpKeyFlags.CanSign | PgpKeyFlags.CanEncryptCommunications);
        hashedGen.SetPreferredCompressionAlgorithms(false, new[] { (int)CompressionAlgorithmTag.Zip });
        hashedGen.SetPreferredHashAlgorithms(false, new[] { (int)HashAlgorithmTag.Sha512 });
        hashedGen.SetPreferredSymmetricAlgorithms(false, new[] { (int)SymmetricKeyAlgorithmTag.Aes256 });
        var unhashedGen = new PgpSignatureSubpacketGenerator();

        // Maak PGP sleutel
        var secretKey = new PgpSecretKey(
            PgpSignature.DefaultCertification,
            PublicKeyAlgorithmTag.RsaGeneral,
            asymmetricCipherKeyPair.Public,
            asymmetricCipherKeyPair.Private,
            DateTime.Now,
            identifier,
            SymmetricKeyAlgorithmTag.Cast5,
            password?.ToCharArray(),
            hashedGen.Generate(),
            unhashedGen.Generate(),
            new SecureRandom());

        var info = new Dictionary<string, string> { { "Tool", "<Your tool name>" }, { "Identifier", identifier } };

        // Extract the keys
        using (var privateMemoryStream = new MemoryStream())
        {
            using (var privateArmoredOutputStream = new ArmoredOutputStream(privateMemoryStream, info))
                secretKey.Encode(privateArmoredOutputStream);

            privateKey = Encoding.ASCII.GetString(privateMemoryStream.ToArray());
        }

        using (var publicMemoryStream = new MemoryStream())
        {
            using (var publicArmoredOutputStream = new ArmoredOutputStream(publicMemoryStream, info))
                secretKey.PublicKey.Encode(publicArmoredOutputStream);

            publicKey = Encoding.ASCII.GetString(publicMemoryStream.ToArray());
        }
    } 
jstedfast commented 2 years ago

I did some digging around in the Cryptography name space and found this class MimeKit.Cryptography.BouncyCastleSecureMimeContext, is this the one to work with certificates?

If you want to use the BouncyCastle backend for S/MIME, then yes.

The SecureMimeContext is the most abstract of the S/MIME classes. The BouncyCastleSecureMimeContext is the subclass that uses BouncyCastle for crypto operations. The WindowsSecureMimeContext is the subclass that sues System.Security for crypto operations.

And also probably a very dumb question, how does MimeKit know if an e-mail is encrypted or signed with a certificate instead of for example a OpenPGP key?

PGP/MIME and S/MIME use different MIME types.

Do I need to do a check like this or is there some code in MimeKit that figures it out?

If you get a MultipartSigned, then calling Verify() will figure out the PGP vs S/MIME stuff for you.

If you get an ApplicationPkcs7Mime part, then that can only be S/MIME and you would check the SecureMimeType property to know if it is signed or encrypted (aka enveloped).

If you get an ApplicationPkcs7Signature, then that is a S/MIME signed-only part.

If you get a MultipartEncrypted, that will generally be a PGP/MIME encrypted part (S/MIME doesn't really use that), but the Decrypt() method will figure out which to use for you.