From a61ac09de392bdfa2a2525557c0c2df699272ac1 Mon Sep 17 00:00:00 2001 From: Jeffrey Stedfast Date: Sat, 11 May 2024 14:02:19 -0400 Subject: [PATCH] Fixed BouncyCastle S/MIME backend to properly encrypt/decrypt for SubjectKeyIdentifier Added Windows -> BouncyCastle and BouncyCastle -> Windows unit tests in order to validate that each backend can decrypt the other backend's output. Fixes https://github.com/bcgit/bc-csharp/issues/532 --- .../BouncyCastleSecureMimeContext.cs | 11 +- MimeKit/MimeKit.csproj | 2 +- UnitTests/Cryptography/SecureMimeTests.cs | 191 +++++++++++++----- 3 files changed, 142 insertions(+), 62 deletions(-) diff --git a/MimeKit/Cryptography/BouncyCastleSecureMimeContext.cs b/MimeKit/Cryptography/BouncyCastleSecureMimeContext.cs index 9f68aa90a4..5b56db3e6c 100644 --- a/MimeKit/Cryptography/BouncyCastleSecureMimeContext.cs +++ b/MimeKit/Cryptography/BouncyCastleSecureMimeContext.cs @@ -48,7 +48,6 @@ using Org.BouncyCastle.Asn1.X509; using Org.BouncyCastle.Asn1.Smime; using Org.BouncyCastle.X509.Store; -using Org.BouncyCastle.Utilities.Date; using Org.BouncyCastle.Crypto.Generators; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Utilities.Collections; @@ -1297,14 +1296,12 @@ public RecipientInfo Generate (KeyParameter contentEncryptionKey, SecureRandom r } var encryptedKeyBytes = GenerateWrappedKey (contentEncryptionKey, keyEncryptionAlgorithm, publicKey, random); - RecipientIdentifier recipientIdentifier = null; + RecipientIdentifier recipientIdentifier; if (recipient.RecipientIdentifierType == SubjectIdentifierType.SubjectKeyIdentifier) { - var subjectKeyIdentifier = recipient.Certificate.GetExtensionValue (X509Extensions.SubjectKeyIdentifier); + var subjectKeyIdentifier = (Asn1OctetString) recipient.Certificate.GetExtensionParsedValue (X509Extensions.SubjectKeyIdentifier); recipientIdentifier = new RecipientIdentifier (subjectKeyIdentifier); - } - - if (recipientIdentifier == null) { + } else { var issuerAndSerial = new IssuerAndSerialNumber (certificate.Issuer, certificate.SerialNumber.Value); recipientIdentifier = new RecipientIdentifier (issuerAndSerial); } @@ -1324,7 +1321,7 @@ void CmsEnvelopeAddEllipticCurve (CmsEnvelopedDataGenerator cms, CmsRecipient re // TODO: better handle algorithm selection. if (recipient.RecipientIdentifierType == SubjectIdentifierType.SubjectKeyIdentifier) { - var subjectKeyIdentifier = recipient.Certificate.GetExtensionValue (X509Extensions.SubjectKeyIdentifier); + var subjectKeyIdentifier = (Asn1OctetString) recipient.Certificate.GetExtensionParsedValue (X509Extensions.SubjectKeyIdentifier); cms.AddKeyAgreementRecipient ( CmsEnvelopedGenerator.ECDHSha1Kdf, keyPair.Private, diff --git a/MimeKit/MimeKit.csproj b/MimeKit/MimeKit.csproj index d5c1b698e5..f6fd17e2c5 100644 --- a/MimeKit/MimeKit.csproj +++ b/MimeKit/MimeKit.csproj @@ -79,7 +79,7 @@ - + diff --git a/UnitTests/Cryptography/SecureMimeTests.cs b/UnitTests/Cryptography/SecureMimeTests.cs index 28d2537c9b..f28090d01a 100644 --- a/UnitTests/Cryptography/SecureMimeTests.cs +++ b/UnitTests/Cryptography/SecureMimeTests.cs @@ -154,76 +154,80 @@ static SecureMimeTestsBase () SMimeCertificates = all.ToArray (); } - protected SecureMimeTestsBase () + protected void ImportTestCertificates (SecureMimeContext ctx) { - if (!IsEnabled) - return; + var dataDir = Path.Combine (TestHelper.ProjectDir, "TestData", "smime"); + var windows = ctx as WindowsSecureMimeContext; - using (var ctx = CreateContext ()) { - var dataDir = Path.Combine (TestHelper.ProjectDir, "TestData", "smime"); - var windows = ctx as WindowsSecureMimeContext; + if (ctx is TemporarySecureMimeContext) + CryptographyContext.Register (CreateContext); + else + CryptographyContext.Register (ctx.GetType ()); - if (ctx is TemporarySecureMimeContext) - CryptographyContext.Register (CreateContext); - else - CryptographyContext.Register (ctx.GetType ()); + // Import the StartCom certificates + if (windows is not null) { + var parser = new X509CertificateParser (); - // Import the StartCom certificates - if (windows is not null) { - var parser = new X509CertificateParser (); + using (var stream = File.OpenRead (Path.Combine (dataDir, "StartComCertificationAuthority.crt"))) { + foreach (X509Certificate certificate in parser.ReadCertificates (stream)) + windows.Import (StoreName.AuthRoot, certificate); + } - using (var stream = File.OpenRead (Path.Combine (dataDir, "StartComCertificationAuthority.crt"))) { - foreach (X509Certificate certificate in parser.ReadCertificates (stream)) - windows.Import (StoreName.AuthRoot, certificate); + using (var stream = File.OpenRead (Path.Combine (dataDir, "StartComClass1PrimaryIntermediateClientCA.crt"))) { + foreach (X509Certificate certificate in parser.ReadCertificates (stream)) + windows.Import (StoreName.CertificateAuthority, certificate); + } + } else { + foreach (var filename in StartComCertificates) { + var path = Path.Combine (dataDir, filename); + using (var stream = File.OpenRead (path)) { + if (ctx is DefaultSecureMimeContext sqlite) { + sqlite.Import (stream, true); + } else { + var parser = new X509CertificateParser (); + foreach (X509Certificate certificate in parser.ReadCertificates (stream)) + ctx.Import (certificate); + } } + } + } + + // Import the smime.pfx certificates + foreach (var mimekitCertificate in SupportedCertificates) { + var chain = mimekitCertificate.Chain; - using (var stream = File.OpenRead (Path.Combine (dataDir, "StartComClass1PrimaryIntermediateClientCA.crt"))) { - foreach (X509Certificate certificate in parser.ReadCertificates (stream)) - windows.Import (StoreName.CertificateAuthority, certificate); + // Import the root & intermediate certificates from the smime.pfx file + if (windows is not null) { + var store = StoreName.AuthRoot; + for (int i = chain.Length - 1; i > 0; i--) { + windows.Import (store, chain[i]); + store = StoreName.CertificateAuthority; } } else { - foreach (var filename in StartComCertificates) { - var path = Path.Combine (dataDir, filename); - using (var stream = File.OpenRead (path)) { - if (ctx is DefaultSecureMimeContext sqlite) { - sqlite.Import (stream, true); - } else { - var parser = new X509CertificateParser (); - foreach (X509Certificate certificate in parser.ReadCertificates (stream)) - ctx.Import (certificate); - } + for (int i = chain.Length - 1; i > 0; i--) { + if (ctx is DefaultSecureMimeContext sqlite) { + sqlite.Import (chain[i], true); + } else { + ctx.Import (chain[i]); } } } - // Import the smime.pfx certificates - foreach (var mimekitCertificate in SupportedCertificates) { - var chain = mimekitCertificate.Chain; + // Import the pfx so that the SecureMimeContext has a copy of the private key as well. + ctx.Import (mimekitCertificate.FileName, "no.secret"); - // Import the root & intermediate certificates from the smime.pfx file - if (windows is not null) { - var store = StoreName.AuthRoot; - for (int i = chain.Length - 1; i > 0; i--) { - windows.Import (store, chain[i]); - store = StoreName.CertificateAuthority; - } - } else { - for (int i = chain.Length - 1; i > 0; i--) { - if (ctx is DefaultSecureMimeContext sqlite) { - sqlite.Import (chain[i], true); - } else { - ctx.Import (chain[i]); - } - } - } + // Import a second time to cover the case where the certificate & private key already exist + Assert.DoesNotThrow (() => ctx.Import (mimekitCertificate.FileName, "no.secret")); + } + } - // Import the pfx so that the SecureMimeContext has a copy of the private key as well. - ctx.Import (mimekitCertificate.FileName, "no.secret"); + protected SecureMimeTestsBase () + { + if (!IsEnabled) + return; - // Import a second time to cover the case where the certificate & private key already exist - Assert.DoesNotThrow (() => ctx.Import (mimekitCertificate.FileName, "no.secret")); - } - } + using (var ctx = CreateContext ()) + ImportTestCertificates (ctx); } public static X509Certificate LoadCertificate (string path) @@ -3010,5 +3014,84 @@ public override Task TestSecureMimeImportExportAsync () return base.TestSecureMimeImportExportAsync (); } + + static ApplicationPkcs7Mime Encrypt (SecureMimeContext ctx, SMimeCertificate certificate, SubjectIdentifierType recipientIdentifierType, MimeEntity entity) + { + var recipients = new CmsRecipientCollection (); + + if (ctx is WindowsSecureMimeContext) + recipients.Add (new CmsRecipient (certificate.Certificate.AsX509Certificate2 (), recipientIdentifierType)); + else + recipients.Add (new CmsRecipient (certificate.Certificate, recipientIdentifierType)); + + return ApplicationPkcs7Mime.Encrypt (ctx, recipients, entity); + } + + static void ValidateCanDecrypt (SecureMimeContext ctx, ApplicationPkcs7Mime encrypted, TextPart expected) + { + using (var stream = new MemoryStream ()) { + ctx.DecryptTo (encrypted.Content.Open (), stream); + stream.Position = 0; + + var decrypted = MimeEntity.Load (stream); + + Assert.That (decrypted, Is.InstanceOf (), "Decrypted part is not the expected type."); + Assert.That (((TextPart) decrypted).Text, Is.EqualTo (expected.Text), "Decrypted content is not the same as the original."); + } + } + + [TestCase (SubjectIdentifierType.IssuerAndSerialNumber)] + [TestCase (SubjectIdentifierType.SubjectKeyIdentifier)] + public void TestBouncyCastleCanDecryptWindows (SubjectIdentifierType recipientIdentifierType) + { + if (!IsEnabled) + return; + + var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up encrypting..." }; + + using (var ctx = CreateContext ()) { + using (var bc = new TemporarySecureMimeContext ()) { + ImportTestCertificates (bc); + + foreach (var certificate in SupportedCertificates) { + if (!Supports (certificate.PublicKeyAlgorithm)) + continue; + + var encrypted = Encrypt (ctx, certificate, recipientIdentifierType, body); + + Assert.That (encrypted.SecureMimeType, Is.EqualTo (SecureMimeType.EnvelopedData), "S/MIME type did not match."); + + ValidateCanDecrypt (bc, encrypted, body); + } + } + } + } + + [TestCase (SubjectIdentifierType.IssuerAndSerialNumber)] + [TestCase (SubjectIdentifierType.SubjectKeyIdentifier)] + public void TestWindowsCanDecryptBouncyCastle (SubjectIdentifierType recipientIdentifierType) + { + if (!IsEnabled) + return; + + var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up encrypting..." }; + + using (var ctx = CreateContext ()) { + using (var bc = new TemporarySecureMimeContext ()) { + ImportTestCertificates (bc); + + foreach (var certificate in SupportedCertificates) { + if (!Supports (certificate.PublicKeyAlgorithm)) + continue; + + var encrypted = Encrypt (bc, certificate, recipientIdentifierType, body); + + Assert.That (encrypted.SecureMimeType, Is.EqualTo (SecureMimeType.EnvelopedData), "S/MIME type did not match."); + + ValidateCanDecrypt (ctx, encrypted, body); + } + } + } + } } }