Skip to content

Commit

Permalink
Fixed BouncyCastle S/MIME backend to properly encrypt/decrypt for Sub…
Browse files Browse the repository at this point in the history
…jectKeyIdentifier

Added Windows -> BouncyCastle and BouncyCastle -> Windows unit tests in order
to validate that each backend can decrypt the other backend's output.

Fixes bcgit/bc-csharp#532
  • Loading branch information
jstedfast committed May 11, 2024
1 parent 0c26349 commit a61ac09
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 62 deletions.
11 changes: 4 additions & 7 deletions MimeKit/Cryptography/BouncyCastleSecureMimeContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion MimeKit/MimeKit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.3.1" />
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0-beta.61" />
</ItemGroup>

<ItemGroup>
Expand Down
191 changes: 137 additions & 54 deletions UnitTests/Cryptography/SecureMimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<TextPart> (), "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);
}
}
}
}
}
}

0 comments on commit a61ac09

Please sign in to comment.