Skip to content

Commit 519623f

Browse files
committed
Fixed WindowsSecureMimeContext to work with domain-bound certificates
Added X509CertificateExtensions.GetSubjectDnsNames()
1 parent aa74cf0 commit 519623f

5 files changed

+332
-8
lines changed

MimeKit/Cryptography/WindowsSecureMimeContext.cs

+39-4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
using System;
2828
using System.IO;
29+
using System.Linq;
2930
using System.Threading;
3031
using System.Threading.Tasks;
3132
using System.Collections.Generic;
@@ -174,7 +175,10 @@ public override bool CanEncrypt (MailboxAddress mailbox, CancellationToken cance
174175
protected virtual X509Certificate2 GetRecipientCertificate (MailboxAddress mailbox)
175176
{
176177
var storeNames = new [] { StoreName.AddressBook, StoreName.My, StoreName.TrustedPeople };
178+
var mailboxDomain = MailboxAddress.IdnMapping.Encode (mailbox.Domain);
179+
var mailboxAddress = mailbox.GetAddress (true);
177180
var secure = mailbox as SecureMailboxAddress;
181+
X509Certificate2 domainCertificate = null;
178182
var now = DateTime.UtcNow;
179183

180184
foreach (var storeName in storeNames) {
@@ -197,8 +201,22 @@ protected virtual X509Certificate2 GetRecipientCertificate (MailboxAddress mailb
197201
} else {
198202
var address = certificate.GetNameInfo (X509NameType.EmailName, false);
199203

200-
if (!address.Equals (mailbox.Address, StringComparison.InvariantCultureIgnoreCase))
204+
if (!string.IsNullOrEmpty (address))
205+
address = MailboxAddress.EncodeAddrspec (address);
206+
207+
if (!address.Equals (mailboxAddress, StringComparison.OrdinalIgnoreCase)) {
208+
// Fall back to matching the domain...
209+
if (domainCertificate == null) {
210+
var domains = certificate.GetSubjectDnsNames (true);
211+
212+
if (domains.Any (domain => domain.Equals (mailboxDomain, StringComparison.OrdinalIgnoreCase))) {
213+
// Cache this certificate. We will only use this if we do not find an exact match based on the full email address.
214+
domainCertificate = certificate;
215+
}
216+
}
217+
201218
continue;
219+
}
202220
}
203221

204222
return certificate;
@@ -208,7 +226,7 @@ protected virtual X509Certificate2 GetRecipientCertificate (MailboxAddress mailb
208226
}
209227
}
210228

211-
return null;
229+
return domainCertificate;
212230
}
213231

214232
/// <summary>
@@ -318,8 +336,11 @@ static RealCmsRecipientCollection GetCmsRecipients (CmsRecipientCollection recip
318336
/// <param name="mailbox">The signer's mailbox address.</param>
319337
protected virtual X509Certificate2 GetSignerCertificate (MailboxAddress mailbox)
320338
{
339+
var mailboxDomain = MailboxAddress.IdnMapping.Encode (mailbox.Domain);
340+
var mailboxAddress = mailbox.GetAddress (true);
321341
var store = new X509Store (StoreName.My, StoreLocation);
322342
var secure = mailbox as SecureMailboxAddress;
343+
X509Certificate2 domainCertificate = null;
323344
var now = DateTime.UtcNow;
324345

325346
store.Open (OpenFlags.ReadOnly);
@@ -342,8 +363,22 @@ protected virtual X509Certificate2 GetSignerCertificate (MailboxAddress mailbox)
342363
} else {
343364
var address = certificate.GetNameInfo (X509NameType.EmailName, false);
344365

345-
if (!address.Equals (mailbox.Address, StringComparison.InvariantCultureIgnoreCase))
366+
if (!string.IsNullOrEmpty (address))
367+
address = MailboxAddress.EncodeAddrspec (address);
368+
369+
if (!address.Equals (mailboxAddress, StringComparison.OrdinalIgnoreCase)) {
370+
// Fall back to matching the domain...
371+
if (domainCertificate == null) {
372+
var domains = certificate.GetSubjectDnsNames (true);
373+
374+
if (domains.Any (domain => domain.Equals (mailboxDomain, StringComparison.OrdinalIgnoreCase))) {
375+
// Cache this certificate. We will only use this if we do not find an exact match based on the full email address.
376+
domainCertificate = certificate;
377+
}
378+
}
379+
346380
continue;
381+
}
347382
}
348383

349384
return certificate;
@@ -352,7 +387,7 @@ protected virtual X509Certificate2 GetSignerCertificate (MailboxAddress mailbox)
352387
store.Close ();
353388
}
354389

355-
return null;
390+
return domainCertificate;
356391
}
357392

358393
AsnEncodedData GetSecureMimeCapabilities ()

MimeKit/Cryptography/X509Certificate2Extensions.cs

+68
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838
using X509Certificate = Org.BouncyCastle.X509.X509Certificate;
3939
using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
40+
using X509Extension = System.Security.Cryptography.X509Certificates.X509Extension;
4041

4142
namespace MimeKit.Cryptography {
4243
/// <summary>
@@ -101,6 +102,73 @@ public static PublicKeyAlgorithm GetPublicKeyAlgorithm (this X509Certificate2 ce
101102
}
102103
}
103104

105+
static string[] GetSubjectAlternativeNames (X509Certificate2 certificate, int tagNo)
106+
{
107+
X509Extension alt = null;
108+
109+
foreach (var extension in certificate.Extensions) {
110+
if (extension.Oid.Value == X509Extensions.SubjectAlternativeName.Id) {
111+
alt = extension;
112+
break;
113+
}
114+
}
115+
116+
if (alt == null)
117+
return Array.Empty<string> ();
118+
119+
using (var memory = new MemoryStream (alt.RawData, false)) {
120+
var seq = Asn1Sequence.GetInstance (Asn1Object.FromByteArray (alt.RawData));
121+
var names = new string[seq.Count];
122+
int count = 0;
123+
124+
foreach (Asn1Encodable encodable in seq) {
125+
var name = GeneralName.GetInstance (encodable);
126+
if (name.TagNo == tagNo)
127+
names[count++] = ((IAsn1String) name.Name).GetString ();
128+
}
129+
130+
if (count == 0)
131+
return Array.Empty<string> ();
132+
133+
if (count < names.Length)
134+
Array.Resize (ref names, count);
135+
136+
return names;
137+
}
138+
}
139+
140+
/// <summary>
141+
/// Get the subject domain names of the certificate.
142+
/// </summary>
143+
/// <remarks>
144+
/// <para>Gets the subject DNS names of the certificate.</para>
145+
/// <para>Some S/MIME certificates are domain-bound instead of being bound to a
146+
/// particular email address.</para>
147+
/// </remarks>
148+
/// <returns>The subject DNS names.</returns>
149+
/// <param name="certificate">The certificate.</param>
150+
/// <param name="idnEncode">If set to <c>true</c>, international domain names will be IDN encoded.</param>
151+
/// <exception cref="System.ArgumentNullException">
152+
/// <paramref name="certificate"/> is <see langword="null"/>.
153+
/// </exception>
154+
public static string[] GetSubjectDnsNames (this X509Certificate2 certificate, bool idnEncode = false)
155+
{
156+
if (certificate == null)
157+
throw new ArgumentNullException (nameof (certificate));
158+
159+
var domains = GetSubjectAlternativeNames (certificate, GeneralName.DnsName);
160+
161+
if (idnEncode) {
162+
for (int i = 0; i < domains.Length; i++)
163+
domains[i] = MailboxAddress.IdnMapping.Encode (domains[i]);
164+
} else {
165+
for (int i = 0; i < domains.Length; i++)
166+
domains[i] = MailboxAddress.IdnMapping.Decode (domains[i]);
167+
}
168+
169+
return domains;
170+
}
171+
104172
static EncryptionAlgorithm[] DecodeEncryptionAlgorithms (byte[] rawData)
105173
{
106174
using (var memory = new MemoryStream (rawData, false)) {

UnitTests/Cryptography/ApplicationPkcs7MimeTests.cs

+138
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,70 @@ public async Task TestEncryptMailboxesAsync ()
468468
}
469469
}
470470

471+
[Test]
472+
public void TestEncryptDnsNames ()
473+
{
474+
var certificate = SecureMimeTestsBase.DomainCertificate;
475+
476+
foreach (var domain in certificate.DnsNames) {
477+
var entity = new TextPart ("plain") { Text = "This is some text..." };
478+
var mailbox = new MailboxAddress ("MimeKit UnitTests", "mimekit@" + domain);
479+
var mailboxes = new[] { mailbox };
480+
481+
using (var ctx = CreateContext ()) {
482+
ApplicationPkcs7Mime encrypted;
483+
MimeEntity decrypted;
484+
TextPart text;
485+
486+
ctx.Import (certificate.FileName, "no.secret");
487+
488+
encrypted = ApplicationPkcs7Mime.Encrypt (mailboxes, entity);
489+
decrypted = encrypted.Decrypt (ctx);
490+
Assert.That (decrypted, Is.InstanceOf<TextPart> (), "Decrypted from Encrypt(mailboxes, entity)");
491+
text = (TextPart) decrypted;
492+
Assert.That (text.Text, Is.EqualTo (entity.Text), "Decrypted text");
493+
494+
encrypted = ApplicationPkcs7Mime.Encrypt (ctx, mailboxes, entity);
495+
decrypted = encrypted.Decrypt (ctx);
496+
Assert.That (decrypted, Is.InstanceOf<TextPart> (), "Encrypt(ctx, mailboxes, entity)");
497+
text = (TextPart) decrypted;
498+
Assert.That (text.Text, Is.EqualTo (entity.Text), "Decrypted text");
499+
}
500+
}
501+
}
502+
503+
[Test]
504+
public async Task TestEncryptDnsNamesAsync ()
505+
{
506+
var certificate = SecureMimeTestsBase.DomainCertificate;
507+
508+
foreach (var domain in certificate.DnsNames) {
509+
var entity = new TextPart ("plain") { Text = "This is some text..." };
510+
var mailbox = new MailboxAddress ("MimeKit UnitTests", "mimekit@" + domain);
511+
var mailboxes = new[] { mailbox };
512+
513+
using (var ctx = CreateContext ()) {
514+
ApplicationPkcs7Mime encrypted;
515+
MimeEntity decrypted;
516+
TextPart text;
517+
518+
await ctx.ImportAsync (certificate.FileName, "no.secret").ConfigureAwait (false);
519+
520+
encrypted = await ApplicationPkcs7Mime.EncryptAsync (mailboxes, entity).ConfigureAwait (false);
521+
decrypted = await encrypted.DecryptAsync (ctx).ConfigureAwait (false);
522+
Assert.That (decrypted, Is.InstanceOf<TextPart> (), "Decrypted from EncryptAsync(mailboxes, entity)");
523+
text = (TextPart) decrypted;
524+
Assert.That (text.Text, Is.EqualTo (entity.Text), "Decrypted text");
525+
526+
encrypted = await ApplicationPkcs7Mime.EncryptAsync (ctx, mailboxes, entity).ConfigureAwait (false);
527+
decrypted = await encrypted.DecryptAsync (ctx).ConfigureAwait (false);
528+
Assert.That (decrypted, Is.InstanceOf<TextPart> (), "EncryptAsync(ctx, mailboxes, entity)");
529+
text = (TextPart) decrypted;
530+
Assert.That (text.Text, Is.EqualTo (entity.Text), "Decrypted text");
531+
}
532+
}
533+
}
534+
471535
void AssertSignResults (SMimeCertificate certificate, SecureMimeContext ctx, ApplicationPkcs7Mime signed, TextPart entity)
472536
{
473537
var signatures = signed.Verify (ctx, out var encapsulated);
@@ -578,6 +642,42 @@ public async Task TestSignMailboxAsync ()
578642
}
579643
}
580644

645+
[Test]
646+
public void TestSignDnsNames ()
647+
{
648+
var certificate = SecureMimeTestsBase.DomainCertificate;
649+
650+
using (var ctx = CreateContext ()) {
651+
ImportAll (ctx);
652+
653+
foreach (var domain in certificate.DnsNames) {
654+
var mailbox = new MailboxAddress ("MimeKit UnitTests", "mimekit@" + domain);
655+
var entity = new TextPart ("plain") { Text = "This is some text..." };
656+
657+
var signed = ApplicationPkcs7Mime.Sign (ctx, mailbox, DigestAlgorithm.Sha224, entity);
658+
AssertSignResults (certificate, ctx, signed, entity);
659+
}
660+
}
661+
}
662+
663+
[Test]
664+
public async Task TestSignDnsNamesAsync ()
665+
{
666+
var certificate = SecureMimeTestsBase.DomainCertificate;
667+
668+
using (var ctx = CreateContext ()) {
669+
await ImportAllAsync (ctx).ConfigureAwait (false);
670+
671+
foreach (var domain in certificate.DnsNames) {
672+
var mailbox = new MailboxAddress ("MimeKit UnitTests", "mimekit@" + domain);
673+
var entity = new TextPart ("plain") { Text = "This is some text..." };
674+
675+
var signed = await ApplicationPkcs7Mime.SignAsync (ctx, mailbox, DigestAlgorithm.Sha224, entity).ConfigureAwait (false);
676+
AssertSignResults (certificate, ctx, signed, entity);
677+
}
678+
}
679+
}
680+
581681
void AssertSignAndEncryptResults (SMimeCertificate certificate, SecureMimeContext ctx, ApplicationPkcs7Mime encrypted, TextPart entity)
582682
{
583683
var decrypted = encrypted.Decrypt (ctx);
@@ -753,6 +853,44 @@ public async Task TestSignAndEncryptMailboxesAsync ()
753853
}
754854
}
755855
}
856+
857+
[Test]
858+
public void TestSignAndEncryptDnsNames ()
859+
{
860+
var certificate = SecureMimeTestsBase.DomainCertificate;
861+
862+
using (var ctx = CreateContext ()) {
863+
ImportAll (ctx);
864+
865+
foreach (var domain in certificate.DnsNames) {
866+
var mailbox = new MailboxAddress ("MimeKit UnitTests", "mimekit@" + domain);
867+
var entity = new TextPart ("plain") { Text = "This is some text..." };
868+
var recipients = new MailboxAddress[] { mailbox };
869+
870+
var encrypted = ApplicationPkcs7Mime.SignAndEncrypt (mailbox, DigestAlgorithm.Sha224, recipients, entity);
871+
AssertSignAndEncryptResults (certificate, ctx, encrypted, entity);
872+
}
873+
}
874+
}
875+
876+
[Test]
877+
public async Task TestSignAndEncryptDnsNamesAsync ()
878+
{
879+
var certificate = SecureMimeTestsBase.DomainCertificate;
880+
881+
using (var ctx = CreateContext ()) {
882+
await ImportAllAsync (ctx).ConfigureAwait (false);
883+
884+
foreach (var domain in certificate.DnsNames) {
885+
var mailbox = new MailboxAddress ("MimeKit UnitTests", "mimekit@" + domain);
886+
var entity = new TextPart ("plain") { Text = "This is some text..." };
887+
var recipients = new MailboxAddress[] { mailbox };
888+
889+
var encrypted = await ApplicationPkcs7Mime.SignAndEncryptAsync (mailbox, DigestAlgorithm.Sha224, recipients, entity).ConfigureAwait (false);
890+
await AssertSignAndEncryptResultsAsync (certificate, ctx, encrypted, entity).ConfigureAwait (false);
891+
}
892+
}
893+
}
756894
}
757895

758896
[TestFixture]

UnitTests/Cryptography/CertificateExtensionTests.cs

+6
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,12 @@ public void TestGetSubjectDnsNames ()
119119
Assert.That (dnsNames.Length, Is.EqualTo (expectedDnsNames.Length), "SubjectDnsNames.Length");
120120
for (int i = 0; i < dnsNames.Length; i++)
121121
Assert.That (dnsNames[i], Is.EqualTo (expectedDnsNames[i]), $"SubjectDnsNames[{i}]");
122+
123+
dnsNames = certificate2.GetSubjectDnsNames ();
124+
125+
Assert.That (dnsNames.Length, Is.EqualTo (expectedDnsNames.Length), "SubjectDnsNames.Length #2");
126+
for (int i = 0; i < dnsNames.Length; i++)
127+
Assert.That (dnsNames[i], Is.EqualTo (expectedDnsNames[i]), $"SubjectDnsNames[{i}] #2");
122128
}
123129
}
124130
}

0 commit comments

Comments
 (0)