Skip to content

Commit 40b1d4d

Browse files
authored
Added support for domain-wide S/MIME certificates (#1113)
* Added support for domain-wide S/MIME certificates Replaces pr #1112 * Added unit test for smime.db upgrade (v1 -> v2) * Updated to allow for multiple DnsNames per certificate Also updated code to use constants for the table and column names. * Generate a certificate with DnsNames and updated tests * Fixed WindowsSecureMimeContext to work with domain-bound certificates Added X509CertificateExtensions.GetSubjectDnsNames()
1 parent 25ee6aa commit 40b1d4d

26 files changed

+1235
-200
lines changed

MimeKit/Cryptography/BouncyCastleCertificateExtensions.cs

+63-14
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,34 @@ public static string GetSubjectName (this X509Certificate certificate)
160160
return certificate.GetSubjectNameInfo (X509Name.Name);
161161
}
162162

163+
static string[] GetSubjectAlternativeNames (X509Certificate certificate, int tagNo)
164+
{
165+
var alt = certificate.GetExtensionValue (X509Extensions.SubjectAlternativeName);
166+
167+
if (alt == null)
168+
return Array.Empty<string> ();
169+
170+
var seq = Asn1Sequence.GetInstance (Asn1Object.FromByteArray (alt.GetOctets ()));
171+
var names = new string[seq.Count];
172+
int count = 0;
173+
174+
foreach (Asn1Encodable encodable in seq) {
175+
var name = GeneralName.GetInstance (encodable);
176+
if (name.TagNo == tagNo)
177+
names[count++] = ((IAsn1String) name.Name).GetString ();
178+
}
179+
180+
if (count == 0)
181+
return Array.Empty<string> ();
182+
183+
if (count < names.Length)
184+
Array.Resize (ref names, count);
185+
186+
return names;
187+
}
188+
163189
/// <summary>
164-
/// Gets the subject email address of the certificate.
190+
/// Get the subject email address of the certificate.
165191
/// </summary>
166192
/// <remarks>
167193
/// The email address component of the certificate's Subject identifier is
@@ -170,31 +196,54 @@ public static string GetSubjectName (this X509Certificate certificate)
170196
/// </remarks>
171197
/// <returns>The subject email address.</returns>
172198
/// <param name="certificate">The certificate.</param>
199+
/// <param name="idnEncode">If set to <c>true</c>, international edomain names will be IDN encoded.</param>
173200
/// <exception cref="System.ArgumentNullException">
174201
/// <paramref name="certificate"/> is <see langword="null"/>.
175202
/// </exception>
176-
public static string GetSubjectEmailAddress (this X509Certificate certificate)
203+
public static string GetSubjectEmailAddress (this X509Certificate certificate, bool idnEncode = false)
177204
{
178205
var address = certificate.GetSubjectNameInfo (X509Name.EmailAddress);
179206

180-
if (!string.IsNullOrEmpty (address))
181-
return address;
182-
183-
var alt = certificate.GetExtensionValue (X509Extensions.SubjectAlternativeName);
207+
if (string.IsNullOrEmpty (address)) {
208+
var addresses = GetSubjectAlternativeNames (certificate, GeneralName.Rfc822Name);
184209

185-
if (alt == null)
186-
return string.Empty;
210+
if (addresses.Length > 0)
211+
address = addresses[0];
212+
}
187213

188-
var seq = Asn1Sequence.GetInstance (Asn1Object.FromByteArray (alt.GetOctets ()));
214+
if (idnEncode && !string.IsNullOrEmpty (address))
215+
address = MailboxAddress.EncodeAddrspec (address);
189216

190-
foreach (Asn1Encodable encodable in seq) {
191-
var name = GeneralName.GetInstance (encodable);
217+
return address;
218+
}
192219

193-
if (name.TagNo == GeneralName.Rfc822Name)
194-
return ((IAsn1String) name.Name).GetString ();
220+
/// <summary>
221+
/// Get the subject domain names of the certificate.
222+
/// </summary>
223+
/// <remarks>
224+
/// <para>Gets the subject DNS names of the certificate.</para>
225+
/// <para>Some S/MIME certificates are domain-bound instead of being bound to a
226+
/// particular email address.</para>
227+
/// </remarks>
228+
/// <returns>The subject DNS names.</returns>
229+
/// <param name="certificate">The certificate.</param>
230+
/// <param name="idnEncode">If set to <c>true</c>, international domain names will be IDN encoded.</param>
231+
/// <exception cref="System.ArgumentNullException">
232+
/// <paramref name="certificate"/> is <see langword="null"/>.
233+
/// </exception>
234+
public static string[] GetSubjectDnsNames (this X509Certificate certificate, bool idnEncode = false)
235+
{
236+
var domains = GetSubjectAlternativeNames (certificate, GeneralName.DnsName);
237+
238+
if (idnEncode) {
239+
for (int i = 0; i < domains.Length; i++)
240+
domains[i] = MailboxAddress.IdnMapping.Encode (domains[i]);
241+
} else {
242+
for (int i = 0; i < domains.Length; i++)
243+
domains[i] = MailboxAddress.IdnMapping.Decode (domains[i]);
195244
}
196245

197-
return null;
246+
return domains;
198247
}
199248

200249
internal static string AsHex (this byte[] blob)

MimeKit/Cryptography/DefaultSecureMimeContext.cs

+42-16
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,16 @@ protected override DateTime GetNextCertificateRevocationListUpdate (X509Name iss
480480
return nextUpdate;
481481
}
482482

483+
static CmsRecipient CreateCmsRecipient (X509CertificateRecord record)
484+
{
485+
var recipient = new CmsRecipient (record.Certificate);
486+
487+
if (record.Algorithms != null)
488+
recipient.EncryptionAlgorithms = record.Algorithms;
489+
490+
return recipient;
491+
}
492+
483493
/// <summary>
484494
/// Gets the <see cref="CmsRecipient"/> for the specified mailbox.
485495
/// </summary>
@@ -497,21 +507,36 @@ protected override DateTime GetNextCertificateRevocationListUpdate (X509Name iss
497507
/// </exception>
498508
protected override CmsRecipient GetCmsRecipient (MailboxAddress mailbox)
499509
{
510+
X509CertificateRecord domain = null;
511+
500512
foreach (var record in dbase.Find (mailbox, DateTime.UtcNow, false, CmsRecipientFields)) {
501513
if (record.KeyUsage != 0 && (record.KeyUsage & X509KeyUsageFlags.KeyEncipherment) == 0)
502514
continue;
503515

504-
var recipient = new CmsRecipient (record.Certificate);
505-
506-
if (record.Algorithms != null)
507-
recipient.EncryptionAlgorithms = record.Algorithms;
516+
if (record.SubjectDnsNames.Length > 0) {
517+
// This is a domain-wide certificate. Only use this if we don't find an exact match for the mailbox address.
518+
domain ??= record;
519+
continue;
520+
}
508521

509-
return recipient;
522+
return CreateCmsRecipient (record);
510523
}
511524

525+
if (domain != null)
526+
return CreateCmsRecipient (domain);
527+
512528
throw new CertificateNotFoundException (mailbox, "A valid certificate could not be found.");
513529
}
514530

531+
CmsSigner CreateCmsSigner (X509CertificateRecord record, DigestAlgorithm digestAlgo)
532+
{
533+
var signer = new CmsSigner (BuildCertificateChain (record.Certificate), record.PrivateKey) {
534+
DigestAlgorithm = digestAlgo
535+
};
536+
537+
return signer;
538+
}
539+
515540
/// <summary>
516541
/// Gets the <see cref="CmsSigner"/> for the specified mailbox.
517542
/// </summary>
@@ -530,26 +555,27 @@ protected override CmsRecipient GetCmsRecipient (MailboxAddress mailbox)
530555
/// </exception>
531556
protected override CmsSigner GetCmsSigner (MailboxAddress mailbox, DigestAlgorithm digestAlgo)
532557
{
533-
AsymmetricKeyParameter privateKey = null;
534-
X509Certificate certificate = null;
558+
X509CertificateRecord domain = null;
535559

536560
foreach (var record in dbase.Find (mailbox, DateTime.UtcNow, true, CmsSignerFields)) {
537561
if (record.KeyUsage != X509KeyUsageFlags.None && (record.KeyUsage & DigitalSignatureKeyUsageFlags) == 0)
538562
continue;
539563

540-
certificate = record.Certificate;
541-
privateKey = record.PrivateKey;
542-
break;
543-
}
564+
if (record.Certificate == null || record.PrivateKey == null)
565+
continue;
544566

545-
if (certificate != null && privateKey != null) {
546-
var signer = new CmsSigner (BuildCertificateChain (certificate), privateKey) {
547-
DigestAlgorithm = digestAlgo
548-
};
567+
if (record.SubjectDnsNames.Length > 0) {
568+
// This is a domain-wide certificate. Only use this if we don't find an exact match for the mailbox address.
569+
domain ??= record;
570+
continue;
571+
}
549572

550-
return signer;
573+
return CreateCmsSigner (record, digestAlgo);
551574
}
552575

576+
if (domain != null)
577+
return CreateCmsSigner (domain, digestAlgo);
578+
553579
throw new CertificateNotFoundException (mailbox, "A valid signing certificate could not be found.");
554580
}
555581

MimeKit/Cryptography/SecureMimeDigitalCertificate.cs

+12-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,18 @@ public string Fingerprint {
130130
/// </remarks>
131131
/// <value>The email address.</value>
132132
public string Email {
133-
get { return Certificate.GetSubjectEmailAddress (); }
133+
get { return Certificate.GetSubjectEmailAddress (true); }
134+
}
135+
136+
/// <summary>
137+
/// Gets the DNS names of the owner of the certificate.
138+
/// </summary>
139+
/// <remarks>
140+
/// Gets the DNS names of the owner of the certificate.
141+
/// </remarks>
142+
/// <value>The DNS name.</value>
143+
public string[] DnsNames {
144+
get { return Certificate.GetSubjectDnsNames (true); }
134145
}
135146

136147
/// <summary>

0 commit comments

Comments
 (0)