Skip to content

Commit

Permalink
Added support for domain-wide S/MIME certificates (#1113)
Browse files Browse the repository at this point in the history
* 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()
  • Loading branch information
jstedfast authored Dec 9, 2024
1 parent 25ee6aa commit 40b1d4d
Show file tree
Hide file tree
Showing 26 changed files with 1,235 additions and 200 deletions.
77 changes: 63 additions & 14 deletions MimeKit/Cryptography/BouncyCastleCertificateExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,34 @@ public static string GetSubjectName (this X509Certificate certificate)
return certificate.GetSubjectNameInfo (X509Name.Name);
}

static string[] GetSubjectAlternativeNames (X509Certificate certificate, int tagNo)
{
var alt = certificate.GetExtensionValue (X509Extensions.SubjectAlternativeName);

if (alt == null)
return Array.Empty<string> ();

var seq = Asn1Sequence.GetInstance (Asn1Object.FromByteArray (alt.GetOctets ()));
var names = new string[seq.Count];
int count = 0;

foreach (Asn1Encodable encodable in seq) {
var name = GeneralName.GetInstance (encodable);
if (name.TagNo == tagNo)
names[count++] = ((IAsn1String) name.Name).GetString ();
}

if (count == 0)
return Array.Empty<string> ();

if (count < names.Length)
Array.Resize (ref names, count);

return names;
}

/// <summary>
/// Gets the subject email address of the certificate.
/// Get the subject email address of the certificate.
/// </summary>
/// <remarks>
/// The email address component of the certificate's Subject identifier is
Expand All @@ -170,31 +196,54 @@ public static string GetSubjectName (this X509Certificate certificate)
/// </remarks>
/// <returns>The subject email address.</returns>
/// <param name="certificate">The certificate.</param>
/// <param name="idnEncode">If set to <c>true</c>, international edomain names will be IDN encoded.</param>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="certificate"/> is <see langword="null"/>.
/// </exception>
public static string GetSubjectEmailAddress (this X509Certificate certificate)
public static string GetSubjectEmailAddress (this X509Certificate certificate, bool idnEncode = false)
{
var address = certificate.GetSubjectNameInfo (X509Name.EmailAddress);

if (!string.IsNullOrEmpty (address))
return address;

var alt = certificate.GetExtensionValue (X509Extensions.SubjectAlternativeName);
if (string.IsNullOrEmpty (address)) {
var addresses = GetSubjectAlternativeNames (certificate, GeneralName.Rfc822Name);

if (alt == null)
return string.Empty;
if (addresses.Length > 0)
address = addresses[0];
}

var seq = Asn1Sequence.GetInstance (Asn1Object.FromByteArray (alt.GetOctets ()));
if (idnEncode && !string.IsNullOrEmpty (address))
address = MailboxAddress.EncodeAddrspec (address);

foreach (Asn1Encodable encodable in seq) {
var name = GeneralName.GetInstance (encodable);
return address;
}

if (name.TagNo == GeneralName.Rfc822Name)
return ((IAsn1String) name.Name).GetString ();
/// <summary>
/// Get the subject domain names of the certificate.
/// </summary>
/// <remarks>
/// <para>Gets the subject DNS names of the certificate.</para>
/// <para>Some S/MIME certificates are domain-bound instead of being bound to a
/// particular email address.</para>
/// </remarks>
/// <returns>The subject DNS names.</returns>
/// <param name="certificate">The certificate.</param>
/// <param name="idnEncode">If set to <c>true</c>, international domain names will be IDN encoded.</param>
/// <exception cref="System.ArgumentNullException">
/// <paramref name="certificate"/> is <see langword="null"/>.
/// </exception>
public static string[] GetSubjectDnsNames (this X509Certificate certificate, bool idnEncode = false)
{
var domains = GetSubjectAlternativeNames (certificate, GeneralName.DnsName);

if (idnEncode) {
for (int i = 0; i < domains.Length; i++)
domains[i] = MailboxAddress.IdnMapping.Encode (domains[i]);
} else {
for (int i = 0; i < domains.Length; i++)
domains[i] = MailboxAddress.IdnMapping.Decode (domains[i]);
}

return null;
return domains;
}

internal static string AsHex (this byte[] blob)
Expand Down
58 changes: 42 additions & 16 deletions MimeKit/Cryptography/DefaultSecureMimeContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,16 @@ protected override DateTime GetNextCertificateRevocationListUpdate (X509Name iss
return nextUpdate;
}

static CmsRecipient CreateCmsRecipient (X509CertificateRecord record)
{
var recipient = new CmsRecipient (record.Certificate);

if (record.Algorithms != null)
recipient.EncryptionAlgorithms = record.Algorithms;

return recipient;
}

/// <summary>
/// Gets the <see cref="CmsRecipient"/> for the specified mailbox.
/// </summary>
Expand All @@ -497,21 +507,36 @@ protected override DateTime GetNextCertificateRevocationListUpdate (X509Name iss
/// </exception>
protected override CmsRecipient GetCmsRecipient (MailboxAddress mailbox)
{
X509CertificateRecord domain = null;

foreach (var record in dbase.Find (mailbox, DateTime.UtcNow, false, CmsRecipientFields)) {
if (record.KeyUsage != 0 && (record.KeyUsage & X509KeyUsageFlags.KeyEncipherment) == 0)
continue;

var recipient = new CmsRecipient (record.Certificate);

if (record.Algorithms != null)
recipient.EncryptionAlgorithms = record.Algorithms;
if (record.SubjectDnsNames.Length > 0) {
// This is a domain-wide certificate. Only use this if we don't find an exact match for the mailbox address.
domain ??= record;
continue;
}

return recipient;
return CreateCmsRecipient (record);
}

if (domain != null)
return CreateCmsRecipient (domain);

throw new CertificateNotFoundException (mailbox, "A valid certificate could not be found.");
}

CmsSigner CreateCmsSigner (X509CertificateRecord record, DigestAlgorithm digestAlgo)
{
var signer = new CmsSigner (BuildCertificateChain (record.Certificate), record.PrivateKey) {
DigestAlgorithm = digestAlgo
};

return signer;
}

/// <summary>
/// Gets the <see cref="CmsSigner"/> for the specified mailbox.
/// </summary>
Expand All @@ -530,26 +555,27 @@ protected override CmsRecipient GetCmsRecipient (MailboxAddress mailbox)
/// </exception>
protected override CmsSigner GetCmsSigner (MailboxAddress mailbox, DigestAlgorithm digestAlgo)
{
AsymmetricKeyParameter privateKey = null;
X509Certificate certificate = null;
X509CertificateRecord domain = null;

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

certificate = record.Certificate;
privateKey = record.PrivateKey;
break;
}
if (record.Certificate == null || record.PrivateKey == null)
continue;

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

return signer;
return CreateCmsSigner (record, digestAlgo);
}

if (domain != null)
return CreateCmsSigner (domain, digestAlgo);

throw new CertificateNotFoundException (mailbox, "A valid signing certificate could not be found.");
}

Expand Down
13 changes: 12 additions & 1 deletion MimeKit/Cryptography/SecureMimeDigitalCertificate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,18 @@ public string Fingerprint {
/// </remarks>
/// <value>The email address.</value>
public string Email {
get { return Certificate.GetSubjectEmailAddress (); }
get { return Certificate.GetSubjectEmailAddress (true); }
}

/// <summary>
/// Gets the DNS names of the owner of the certificate.
/// </summary>
/// <remarks>
/// Gets the DNS names of the owner of the certificate.
/// </remarks>
/// <value>The DNS name.</value>
public string[] DnsNames {
get { return Certificate.GetSubjectDnsNames (true); }
}

/// <summary>
Expand Down
Loading

0 comments on commit 40b1d4d

Please sign in to comment.