diff --git a/MimeKit/Cryptography/BouncyCastleCertificateExtensions.cs b/MimeKit/Cryptography/BouncyCastleCertificateExtensions.cs index 7d8e61740e..3215e26b21 100644 --- a/MimeKit/Cryptography/BouncyCastleCertificateExtensions.cs +++ b/MimeKit/Cryptography/BouncyCastleCertificateExtensions.cs @@ -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 (); + + 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 (); + + if (count < names.Length) + Array.Resize (ref names, count); + + return names; + } + /// - /// Gets the subject email address of the certificate. + /// Get the subject email address of the certificate. /// /// /// The email address component of the certificate's Subject identifier is @@ -170,31 +196,54 @@ public static string GetSubjectName (this X509Certificate certificate) /// /// The subject email address. /// The certificate. + /// If set to true, international edomain names will be IDN encoded. /// /// is . /// - 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 (); + /// + /// Get the subject domain names of the certificate. + /// + /// + /// Gets the subject DNS names of the certificate. + /// Some S/MIME certificates are domain-bound instead of being bound to a + /// particular email address. + /// + /// The subject DNS names. + /// The certificate. + /// If set to true, international domain names will be IDN encoded. + /// + /// is . + /// + 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) diff --git a/MimeKit/Cryptography/DefaultSecureMimeContext.cs b/MimeKit/Cryptography/DefaultSecureMimeContext.cs index ca4c65da6e..8954f95ab4 100644 --- a/MimeKit/Cryptography/DefaultSecureMimeContext.cs +++ b/MimeKit/Cryptography/DefaultSecureMimeContext.cs @@ -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; + } + /// /// Gets the for the specified mailbox. /// @@ -497,21 +507,36 @@ protected override DateTime GetNextCertificateRevocationListUpdate (X509Name iss /// 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; + } + /// /// Gets the for the specified mailbox. /// @@ -530,26 +555,27 @@ protected override CmsRecipient GetCmsRecipient (MailboxAddress mailbox) /// 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."); } diff --git a/MimeKit/Cryptography/SecureMimeDigitalCertificate.cs b/MimeKit/Cryptography/SecureMimeDigitalCertificate.cs index 2991d20347..0c4b856e5d 100644 --- a/MimeKit/Cryptography/SecureMimeDigitalCertificate.cs +++ b/MimeKit/Cryptography/SecureMimeDigitalCertificate.cs @@ -130,7 +130,18 @@ public string Fingerprint { /// /// The email address. public string Email { - get { return Certificate.GetSubjectEmailAddress (); } + get { return Certificate.GetSubjectEmailAddress (true); } + } + + /// + /// Gets the DNS names of the owner of the certificate. + /// + /// + /// Gets the DNS names of the owner of the certificate. + /// + /// The DNS name. + public string[] DnsNames { + get { return Certificate.GetSubjectDnsNames (true); } } /// diff --git a/MimeKit/Cryptography/SqlCertificateDatabase.cs b/MimeKit/Cryptography/SqlCertificateDatabase.cs index 9b5a215bbf..0bfa7d6956 100644 --- a/MimeKit/Cryptography/SqlCertificateDatabase.cs +++ b/MimeKit/Cryptography/SqlCertificateDatabase.cs @@ -95,15 +95,15 @@ protected SqlCertificateDatabase (DbConnection connection, string password, Secu if (connection.State != ConnectionState.Open) connection.Open (); - CertificatesTable = CreateCertificatesDataTable ("CERTIFICATES"); - CrlsTable = CreateCrlsDataTable ("CRLS"); + CertificatesTable = CreateCertificatesDataTable (CertificatesTableName); + CrlsTable = CreateCrlsDataTable (CrlsTableName); CreateCertificatesTable (connection, CertificatesTable); CreateCrlsTable (connection, CrlsTable); } /// - /// Gets the X.509 certificate table definition. + /// Get the X.509 certificate table definition. /// /// /// Gets the X.509 certificate table definition. @@ -114,7 +114,7 @@ protected DataTable CertificatesTable { } /// - /// Gets the X.509 certificate revocation lists (CRLs) table definition. + /// Get the X.509 certificate revocation lists (CRLs) table definition. /// /// /// Gets the X.509 certificate revocation lists (CRLs) table definition. @@ -127,23 +127,24 @@ protected DataTable CrlsTable { static DataTable CreateCertificatesDataTable (string tableName) { var table = new DataTable (tableName); - table.Columns.Add (new DataColumn ("ID", typeof (int)) { AutoIncrement = true }); - table.Columns.Add (new DataColumn ("TRUSTED", typeof (bool)) { AllowDBNull = false }); - table.Columns.Add (new DataColumn ("ANCHOR", typeof (bool)) { AllowDBNull = false }); - table.Columns.Add (new DataColumn ("BASICCONSTRAINTS", typeof (int)) { AllowDBNull = false }); - table.Columns.Add (new DataColumn ("KEYUSAGE", typeof (int)) { AllowDBNull = false }); - table.Columns.Add (new DataColumn ("NOTBEFORE", typeof (long)) { AllowDBNull = false }); - table.Columns.Add (new DataColumn ("NOTAFTER", typeof (long)) { AllowDBNull = false }); - table.Columns.Add (new DataColumn ("ISSUERNAME", typeof (string)) { AllowDBNull = false }); - table.Columns.Add (new DataColumn ("SERIALNUMBER", typeof (string)) { AllowDBNull = false }); - table.Columns.Add (new DataColumn ("SUBJECTNAME", typeof (string)) { AllowDBNull = false }); - table.Columns.Add (new DataColumn ("SUBJECTKEYIDENTIFIER", typeof (string)) { AllowDBNull = true }); - table.Columns.Add (new DataColumn ("SUBJECTEMAIL", typeof (string)) { AllowDBNull = true }); - table.Columns.Add (new DataColumn ("FINGERPRINT", typeof (string)) { AllowDBNull = false }); - table.Columns.Add (new DataColumn ("ALGORITHMS", typeof (string)) { AllowDBNull = true }); - table.Columns.Add (new DataColumn ("ALGORITHMSUPDATED", typeof (long)) { AllowDBNull = false }); - table.Columns.Add (new DataColumn ("CERTIFICATE", typeof (byte[])) { AllowDBNull = false, Unique = true }); - table.Columns.Add (new DataColumn ("PRIVATEKEY", typeof (byte[])) { AllowDBNull = true }); + table.Columns.Add (new DataColumn (CertificateColumnNames.Id, typeof (int)) { AutoIncrement = true }); + table.Columns.Add (new DataColumn (CertificateColumnNames.Trusted, typeof (bool)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn (CertificateColumnNames.Anchor, typeof (bool)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn (CertificateColumnNames.BasicConstraints, typeof (int)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn (CertificateColumnNames.KeyUsage, typeof (int)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn (CertificateColumnNames.NotBefore, typeof (long)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn (CertificateColumnNames.NotAfter, typeof (long)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn (CertificateColumnNames.IssuerName, typeof (string)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn (CertificateColumnNames.SerialNumber, typeof (string)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn (CertificateColumnNames.SubjectName, typeof (string)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn (CertificateColumnNames.SubjectKeyIdentifier, typeof (string)) { AllowDBNull = true }); + table.Columns.Add (new DataColumn (CertificateColumnNames.SubjectEmail, typeof (string)) { AllowDBNull = true }); + table.Columns.Add (new DataColumn (CertificateColumnNames.SubjectDnsNames, typeof (string)) { AllowDBNull = true }); + table.Columns.Add (new DataColumn (CertificateColumnNames.Fingerprint, typeof (string)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn (CertificateColumnNames.Algorithms, typeof (string)) { AllowDBNull = true }); + table.Columns.Add (new DataColumn (CertificateColumnNames.AlgorithmsUpdated, typeof (long)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn (CertificateColumnNames.Certificate, typeof (byte[])) { AllowDBNull = false, Unique = true }); + table.Columns.Add (new DataColumn (CertificateColumnNames.PrivateKey, typeof (byte[])) { AllowDBNull = true }); table.PrimaryKey = new DataColumn[] { table.Columns[0] }; return table; @@ -152,12 +153,12 @@ static DataTable CreateCertificatesDataTable (string tableName) static DataTable CreateCrlsDataTable (string tableName) { var table = new DataTable (tableName); - table.Columns.Add (new DataColumn ("ID", typeof (int)) { AutoIncrement = true }); - table.Columns.Add (new DataColumn ("DELTA", typeof (bool)) { AllowDBNull = false }); - table.Columns.Add (new DataColumn ("ISSUERNAME", typeof (string)) { AllowDBNull = false }); - table.Columns.Add (new DataColumn ("THISUPDATE", typeof (long)) { AllowDBNull = false }); - table.Columns.Add (new DataColumn ("NEXTUPDATE", typeof (long)) { AllowDBNull = false }); - table.Columns.Add (new DataColumn ("CRL", typeof (byte[])) { AllowDBNull = false }); + table.Columns.Add (new DataColumn (CrlColumnNames.Id, typeof (int)) { AutoIncrement = true }); + table.Columns.Add (new DataColumn (CrlColumnNames.Delta, typeof (bool)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn (CrlColumnNames.IssuerName, typeof (string)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn (CrlColumnNames.ThisUpdate, typeof (long)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn (CrlColumnNames.NextUpdate, typeof (long)) { AllowDBNull = false }); + table.Columns.Add (new DataColumn (CrlColumnNames.Crl, typeof (byte[])) { AllowDBNull = false }); table.PrimaryKey = new DataColumn[] { table.Columns[0] }; return table; @@ -218,7 +219,7 @@ protected static string GetIndexName (string tableName, string[] columnNames) /// The database connection. /// The name of the table. /// The names of the columns to index. - protected virtual void CreateIndex (DbConnection connection, string tableName, string[] columnNames) + protected virtual void CreateIndex (DbConnection connection, string tableName, params string[] columnNames) { var indexName = GetIndexName (tableName, columnNames); var query = string.Format ("CREATE INDEX IF NOT EXISTS {0} ON {1}({2})", indexName, tableName, string.Join (", ", columnNames)); @@ -238,7 +239,7 @@ protected virtual void CreateIndex (DbConnection connection, string tableName, s /// The database connection. /// The name of the table. /// The names of the columns that were indexed. - protected virtual void RemoveIndex (DbConnection connection, string tableName, string[] columnNames) + protected virtual void RemoveIndex (DbConnection connection, string tableName, params string[] columnNames) { var indexName = GetIndexName (tableName, columnNames); var query = string.Format ("DROP INDEX IF EXISTS {0}", indexName); @@ -254,37 +255,50 @@ void CreateCertificatesTable (DbConnection connection, DataTable table) CreateTable (connection, table); var currentColumns = GetTableColumns (connection, table.TableName); + bool hasSubjectDnsNamesColumn = false; bool hasAnchorColumn = false; + // Figure out which columns are missing... for (int i = 0; i < currentColumns.Count; i++) { - if (currentColumns[i].ColumnName.Equals ("ANCHOR", StringComparison.Ordinal)) { + if (currentColumns[i].ColumnName.Equals (CertificateColumnNames.SubjectDnsNames, StringComparison.Ordinal)) + hasSubjectDnsNamesColumn = true; + else if (currentColumns[i].ColumnName.Equals (CertificateColumnNames.Anchor, StringComparison.Ordinal)) hasAnchorColumn = true; - break; - } } - // Note: The ANCHOR, SUBJECTNAME and SUBJECTKEYIDENTIFIER columns were all added in the same version, - // so if the ANCHOR column is missing, they all are. + // Certificates Table Version History: + // + // * Version 0: Initial version. + // * Version 1: v2.5.0 added the ANCHOR, SUBJECTNAME, and SUBJECTKEYIDENTIFIER columns. + // * Version 2: v4.9.0 added the SUBJECTDNSNAMES column and started canonicalizing the SUBJECTEMAIL and SUBJECTDNSNAMES columns with the IDN-encoded values. + if (!hasAnchorColumn) { + // Upgrade from Version 1. using (var transaction = connection.BeginTransaction ()) { try { - var column = table.Columns[table.Columns.IndexOf ("ANCHOR")]; + var column = table.Columns[table.Columns.IndexOf (CertificateColumnNames.Anchor)]; AddTableColumn (connection, table, column); - column = table.Columns[table.Columns.IndexOf ("SUBJECTNAME")]; + column = table.Columns[table.Columns.IndexOf (CertificateColumnNames.SubjectName)]; AddTableColumn (connection, table, column); - column = table.Columns[table.Columns.IndexOf ("SUBJECTKEYIDENTIFIER")]; + column = table.Columns[table.Columns.IndexOf (CertificateColumnNames.SubjectKeyIdentifier)]; + AddTableColumn (connection, table, column); + + // Note: The SubjectEmail column exists, but the SubjectDnsNames column was added later, so make sure to add that. + column = table.Columns[table.Columns.IndexOf (CertificateColumnNames.SubjectDnsNames)]; AddTableColumn (connection, table, column); foreach (var record in Find (null, false, X509CertificateRecordFields.Id | X509CertificateRecordFields.Certificate)) { - var statement = "UPDATE CERTIFICATES SET ANCHOR = @ANCHOR, SUBJECTNAME = @SUBJECTNAME, SUBJECTKEYIDENTIFIER = @SUBJECTKEYIDENTIFIER WHERE ID = @ID"; + var statement = $"UPDATE {CertificatesTableName} SET {CertificateColumnNames.Anchor} = @ANCHOR, {CertificateColumnNames.SubjectName} = @SUBJECTNAME, {CertificateColumnNames.SubjectKeyIdentifier} = @SUBJECTKEYIDENTIFIER, {CertificateColumnNames.SubjectEmail} = @SUBJECTEMAIL, {CertificateColumnNames.SubjectDnsNames} = @SUBJECTDNSNAMES WHERE {CertificateColumnNames.Id} = @ID"; using (var command = connection.CreateCommand ()) { command.AddParameterWithValue ("@ID", record.Id); command.AddParameterWithValue ("@ANCHOR", record.IsAnchor); command.AddParameterWithValue ("@SUBJECTNAME", record.SubjectName); command.AddParameterWithValue ("@SUBJECTKEYIDENTIFIER", record.SubjectKeyIdentifier?.AsHex ()); + command.AddParameterWithValue ("@SUBJECTEMAIL", record.SubjectEmail); + command.AddParameterWithValue ("@SUBJECTDNSNAMES", EncodeDnsNames (record.SubjectDnsNames)); command.CommandType = CommandType.Text; command.CommandText = statement; @@ -300,34 +314,64 @@ void CreateCertificatesTable (DbConnection connection, DataTable table) } // Remove some old indexes - RemoveIndex (connection, table.TableName, new[] { "TRUSTED" }); - RemoveIndex (connection, table.TableName, new[] { "TRUSTED", "BASICCONSTRAINTS", "ISSUERNAME", "SERIALNUMBER" }); - RemoveIndex (connection, table.TableName, new[] { "BASICCONSTRAINTS", "ISSUERNAME", "SERIALNUMBER" }); - RemoveIndex (connection, table.TableName, new[] { "BASICCONSTRAINTS", "FINGERPRINT" }); - RemoveIndex (connection, table.TableName, new[] { "BASICCONSTRAINTS", "SUBJECTEMAIL" }); + RemoveIndex (connection, table.TableName, CertificateColumnNames.Trusted); + RemoveIndex (connection, table.TableName, CertificateColumnNames.Trusted, CertificateColumnNames.BasicConstraints, CertificateColumnNames.IssuerName, CertificateColumnNames.SerialNumber); + RemoveIndex (connection, table.TableName, CertificateColumnNames.BasicConstraints, CertificateColumnNames.IssuerName, CertificateColumnNames.SerialNumber); + RemoveIndex (connection, table.TableName, CertificateColumnNames.BasicConstraints, CertificateColumnNames.Fingerprint); + RemoveIndex (connection, table.TableName, CertificateColumnNames.BasicConstraints, CertificateColumnNames.SubjectEmail); + } else if (!hasSubjectDnsNamesColumn) { + // Upgrade from Version 2. + using (var transaction = connection.BeginTransaction ()) { + try { + var column = table.Columns[table.Columns.IndexOf (CertificateColumnNames.SubjectDnsNames)]; + AddTableColumn (connection, table, column); + + foreach (var record in Find (null, false, X509CertificateRecordFields.Id | X509CertificateRecordFields.Certificate)) { + var statement = $"UPDATE {CertificatesTableName} SET {CertificateColumnNames.SubjectEmail} = @SUBJECTEMAIL, {CertificateColumnNames.SubjectDnsNames} = @SUBJECTDNSNAMES WHERE {CertificateColumnNames.Id} = @ID"; + + using (var command = connection.CreateCommand ()) { + command.AddParameterWithValue ("@ID", record.Id); + command.AddParameterWithValue ("@SUBJECTEMAIL", record.SubjectEmail); + command.AddParameterWithValue ("@SUBJECTDNSNAMES", EncodeDnsNames (record.SubjectDnsNames)); + command.CommandType = CommandType.Text; + command.CommandText = statement; + + command.ExecuteNonQuery (); + } + } + + transaction.Commit (); + } catch { + transaction.Rollback (); + throw; + } + } + + // Remove some old indexes + RemoveIndex (connection, table.TableName, CertificateColumnNames.BasicConstraints, CertificateColumnNames.SubjectEmail, CertificateColumnNames.NotBefore, CertificateColumnNames.NotAfter); } // Note: Use "EXPLAIN QUERY PLAN SELECT ... FROM CERTIFICATES WHERE ..." to verify that any indexes we create get used as expected. // Index for matching against a specific certificate - CreateIndex (connection, table.TableName, new [] { "ISSUERNAME", "SERIALNUMBER", "FINGERPRINT" }); + CreateIndex (connection, table.TableName, CertificateColumnNames.IssuerName, CertificateColumnNames.SerialNumber, CertificateColumnNames.Fingerprint); // Index for searching for a certificate based on a SecureMailboxAddress - CreateIndex (connection, table.TableName, new [] { "BASICCONSTRAINTS", "FINGERPRINT", "NOTBEFORE", "NOTAFTER" }); + CreateIndex (connection, table.TableName, CertificateColumnNames.BasicConstraints, CertificateColumnNames.Fingerprint, CertificateColumnNames.NotBefore, CertificateColumnNames.NotAfter); // Index for searching for a certificate based on a MailboxAddress - CreateIndex (connection, table.TableName, new [] { "BASICCONSTRAINTS", "SUBJECTEMAIL", "NOTBEFORE", "NOTAFTER" }); + CreateIndex (connection, table.TableName, CertificateColumnNames.BasicConstraints, CertificateColumnNames.SubjectEmail, CertificateColumnNames.SubjectDnsNames, CertificateColumnNames.NotBefore, CertificateColumnNames.NotAfter); // Index for gathering a list of Trusted Anchors - CreateIndex (connection, table.TableName, new [] { "TRUSTED", "ANCHOR", "KEYUSAGE" }); + CreateIndex (connection, table.TableName, CertificateColumnNames.Trusted, CertificateColumnNames.Anchor, CertificateColumnNames.KeyUsage); } void CreateCrlsTable (DbConnection connection, DataTable table) { CreateTable (connection, table); - CreateIndex (connection, table.TableName, new [] { "ISSUERNAME" }); - CreateIndex (connection, table.TableName, new [] { "DELTA", "ISSUERNAME", "THISUPDATE" }); + CreateIndex (connection, table.TableName, CrlColumnNames.IssuerName); + CreateIndex (connection, table.TableName, CrlColumnNames.Delta, CrlColumnNames.IssuerName, CrlColumnNames.ThisUpdate); } /// @@ -350,7 +394,30 @@ protected static StringBuilder CreateSelectQuery (X509CertificateRecordFields fi query = query.Append (columns[i]); } - return query.Append (" FROM CERTIFICATES"); + return query.Append (" FROM ").Append (CertificatesTableName); + } + + /// + /// Creates a SELECT query string builder for the specified fields of an X.509 CRL record. + /// + /// + /// Creates a SELECT query string builder for the specified fields of an X.509 CRL record. + /// + /// The X.509 CRL fields. + /// A containing a basic SELECT query string. + protected static StringBuilder CreateSelectQuery (X509CrlRecordFields fields) + { + var query = new StringBuilder ("SELECT "); + var columns = GetColumnNames (fields); + + for (int i = 0; i < columns.Length; i++) { + if (i > 0) + query = query.Append (", "); + + query = query.Append (columns[i]); + } + + return query.Append (" FROM ").Append (CrlsTableName); } /// @@ -372,7 +439,10 @@ protected override DbCommand GetSelectCommand (DbConnection connection, X509Cert var query = CreateSelectQuery (fields); // FIXME: Is this really the best way to query for an exact match of a certificate? - query = query.Append (" WHERE ISSUERNAME = @ISSUERNAME AND SERIALNUMBER = @SERIALNUMBER AND FINGERPRINT = @FINGERPRINT LIMIT 1"); + query = query.Append (" WHERE ") + .Append (CertificateColumnNames.IssuerName).Append (" = @ISSUERNAME AND ") + .Append (CertificateColumnNames.SerialNumber).Append (" = @SERIALNUMBER AND ") + .Append (CertificateColumnNames.Fingerprint).Append (" = @FINGERPRINT LIMIT 1"); command.AddParameterWithValue ("@ISSUERNAME", issuerName); command.AddParameterWithValue ("@SERIALNUMBER", serialNumber); command.AddParameterWithValue ("@FINGERPRINT", fingerprint); @@ -400,27 +470,36 @@ protected override DbCommand GetSelectCommand (DbConnection connection, MailboxA var command = connection.CreateCommand (); var query = CreateSelectQuery (fields); - query = query.Append (" WHERE BASICCONSTRAINTS = @BASICCONSTRAINTS "); + query = query.Append (" WHERE ").Append (CertificateColumnNames.BasicConstraints).Append (" = @BASICCONSTRAINTS "); command.AddParameterWithValue ("@BASICCONSTRAINTS", -1); if (mailbox is SecureMailboxAddress secure && !string.IsNullOrEmpty (secure.Fingerprint)) { if (secure.Fingerprint.Length < 40) { command.AddParameterWithValue ("@FINGERPRINT", secure.Fingerprint.ToLowerInvariant () + "%"); - query = query.Append ("AND FINGERPRINT LIKE @FINGERPRINT "); + query = query.Append ("AND ").Append (CertificateColumnNames.Fingerprint).Append (" LIKE @FINGERPRINT "); } else { command.AddParameterWithValue ("@FINGERPRINT", secure.Fingerprint.ToLowerInvariant ()); - query = query.Append ("AND FINGERPRINT = @FINGERPRINT "); + query = query.Append ("AND ").Append (CertificateColumnNames.Fingerprint).Append (" = @FINGERPRINT "); } } else { - command.AddParameterWithValue ("@SUBJECTEMAIL", mailbox.Address.ToLowerInvariant ()); - query = query.Append ("AND SUBJECTEMAIL = @SUBJECTEMAIL "); + var domain = MailboxAddress.IdnMapping.Encode (mailbox.Domain); + var address = mailbox.GetAddress (true); + + command.AddParameterWithValue ("@SUBJECTEMAIL", address.ToLowerInvariant ()); + command.AddParameterWithValue ("@SUBJECTDNSNAME", $"%|{domain.ToLowerInvariant ()}|%"); + + query = query.Append ("AND (") + .Append (CertificateColumnNames.SubjectEmail).Append ("= @SUBJECTEMAIL OR ") + .Append (CertificateColumnNames.SubjectDnsNames).Append (" LIKE @SUBJECTDNSNAME) "); } - query = query.Append ("AND NOTBEFORE < @NOW AND NOTAFTER > @NOW"); + query = query.Append ("AND ") + .Append (CertificateColumnNames.NotBefore).Append (" < @NOW AND ") + .Append (CertificateColumnNames.NotAfter).Append (" > @NOW"); command.AddParameterWithValue ("@NOW", now.ToUniversalTime ()); if (requirePrivateKey) - query = query.Append (" AND PRIVATEKEY IS NOT NULL"); + query = query.Append (" AND ").Append (CertificateColumnNames.PrivateKey).Append (" IS NOT NULL"); command.CommandText = query.ToString (); command.CommandType = CommandType.Text; @@ -452,7 +531,8 @@ protected override DbCommand GetSelectCommand (DbConnection connection, ISelecto // adds properties like bool Trusted, bool Anchor, and bool HasPrivateKey ? Then we could drop the // bool method arguments... if (trustedAnchorsOnly) { - query = query.Append ("TRUSTED = @TRUSTED AND ANCHOR = @ANCHOR"); + query = query.Append (CertificateColumnNames.Trusted).Append (" = @TRUSTED AND ") + .Append (CertificateColumnNames.Anchor).Append (" = @ANCHOR"); command.AddParameterWithValue ("@TRUSTED", true); command.AddParameterWithValue ("@ANCHOR", true); } @@ -464,10 +544,10 @@ protected override DbCommand GetSelectCommand (DbConnection connection, ISelecto if (match.BasicConstraints == -2) { command.AddParameterWithValue ("@BASICCONSTRAINTS", -1); - query = query.Append ("BASICCONSTRAINTS = @BASICCONSTRAINTS"); + query = query.Append (CertificateColumnNames.BasicConstraints).Append (" = @BASICCONSTRAINTS"); } else { command.AddParameterWithValue ("@BASICCONSTRAINTS", match.BasicConstraints); - query = query.Append ("BASICCONSTRAINTS >= @BASICCONSTRAINTS"); + query = query.Append (CertificateColumnNames.BasicConstraints).Append (" >= @BASICCONSTRAINTS"); } } @@ -476,7 +556,8 @@ protected override DbCommand GetSelectCommand (DbConnection connection, ISelecto query = query.Append (" AND "); command.AddParameterWithValue ("@DATETIME", match.CertificateValid.Value.ToUniversalTime ()); - query = query.Append ("NOTBEFORE < @DATETIME AND NOTAFTER > @DATETIME"); + query = query.Append (CertificateColumnNames.NotBefore).Append (" < @DATETIME AND ") + .Append (CertificateColumnNames.NotAfter).Append (" > @DATETIME"); } if (match.Issuer != null || match.Certificate != null) { @@ -488,7 +569,7 @@ protected override DbCommand GetSelectCommand (DbConnection connection, ISelecto query = query.Append (" AND "); command.AddParameterWithValue ("@ISSUERNAME", issuer.ToString ()); - query = query.Append ("ISSUERNAME = @ISSUERNAME"); + query = query.Append (CertificateColumnNames.IssuerName).Append (" = @ISSUERNAME"); } if (match.SerialNumber != null || match.Certificate != null) { @@ -500,7 +581,7 @@ protected override DbCommand GetSelectCommand (DbConnection connection, ISelecto query = query.Append (" AND "); command.AddParameterWithValue ("@SERIALNUMBER", serialNumber.ToString ()); - query = query.Append ("SERIALNUMBER = @SERIALNUMBER"); + query = query.Append (CertificateColumnNames.SerialNumber).Append (" = @SERIALNUMBER"); } if (match.Certificate != null) { @@ -510,7 +591,7 @@ protected override DbCommand GetSelectCommand (DbConnection connection, ISelecto query = query.Append (" AND "); command.AddParameterWithValue ("@FINGERPRINT", match.Certificate.GetFingerprint ()); - query = query.Append ("FINGERPRINT = @FINGERPRINT"); + query = query.Append (CertificateColumnNames.Fingerprint).Append (" = @FINGERPRINT"); } if (match.Subject != null) { @@ -518,7 +599,7 @@ protected override DbCommand GetSelectCommand (DbConnection connection, ISelecto query = query.Append (" AND "); command.AddParameterWithValue ("@SUBJECTNAME", match.Subject.ToString ()); - query = query.Append ("SUBJECTNAME = @SUBJECTNAME"); + query = query.Append (CertificateColumnNames.SubjectName).Append (" = @SUBJECTNAME"); } if (match.SubjectKeyIdentifier != null) { @@ -529,7 +610,7 @@ protected override DbCommand GetSelectCommand (DbConnection connection, ISelecto var subjectKeyIdentifier = id.GetOctets ().AsHex (); command.AddParameterWithValue ("@SUBJECTKEYIDENTIFIER", subjectKeyIdentifier); - query = query.Append ("SUBJECTKEYIDENTIFIER = @SUBJECTKEYIDENTIFIER"); + query = query.Append (CertificateColumnNames.SubjectKeyIdentifier).Append (" = @SUBJECTKEYIDENTIFIER"); } if (match.KeyUsage != null) { @@ -540,7 +621,8 @@ protected override DbCommand GetSelectCommand (DbConnection connection, ISelecto query = query.Append (" AND "); command.AddParameterWithValue ("@FLAGS", (int) flags); - query = query.Append ("(KEYUSAGE = 0 OR (KEYUSAGE & @FLAGS) = @FLAGS)"); + query = query.Append ('(').Append (CertificateColumnNames.KeyUsage).Append (" = 0 OR (") + .Append (CertificateColumnNames.KeyUsage).Append (" & @FLAGS) = @FLAGS)"); } } } @@ -549,7 +631,7 @@ protected override DbCommand GetSelectCommand (DbConnection connection, ISelecto if (command.Parameters.Count > 0) query = query.Append (" AND "); - query = query.Append ("PRIVATEKEY IS NOT NULL"); + query = query.Append (CertificateColumnNames.PrivateKey).Append (" IS NOT NULL"); } else if (command.Parameters.Count == 0) { query.Length = baseQueryLength; } @@ -572,10 +654,10 @@ protected override DbCommand GetSelectCommand (DbConnection connection, ISelecto /// The fields to return. protected override DbCommand GetSelectCommand (DbConnection connection, X509Name issuer, X509CrlRecordFields fields) { - var query = "SELECT " + string.Join (", ", GetColumnNames (fields)) + " FROM CRLS "; + var query = CreateSelectQuery (fields).Append (" WHERE ").Append (CrlColumnNames.IssuerName).Append (" = @ISSUERNAME"); var command = connection.CreateCommand (); - command.CommandText = query + "WHERE ISSUERNAME = @ISSUERNAME"; + command.CommandText = query.ToString (); command.AddParameterWithValue ("@ISSUERNAME", issuer.ToString ()); command.CommandType = CommandType.Text; @@ -594,11 +676,14 @@ protected override DbCommand GetSelectCommand (DbConnection connection, X509Name /// The fields to return. protected override DbCommand GetSelectCommand (DbConnection connection, X509Crl crl, X509CrlRecordFields fields) { - var query = "SELECT " + string.Join (", ", GetColumnNames (fields)) + " FROM CRLS "; + var query = CreateSelectQuery (fields).Append (" WHERE ") + .Append (CrlColumnNames.Delta).Append (" = @DELTA AND ") + .Append (CrlColumnNames.IssuerName).Append ("= @ISSUERNAME AND ") + .Append (CrlColumnNames.ThisUpdate).Append (" = @THISUPDATE LIMIT 1"); var issuerName = crl.IssuerDN.ToString (); var command = connection.CreateCommand (); - command.CommandText = query + "WHERE DELTA = @DELTA AND ISSUERNAME = @ISSUERNAME AND THISUPDATE = @THISUPDATE LIMIT 1"; + command.CommandText = query.ToString (); command.AddParameterWithValue ("@DELTA", crl.IsDelta ()); command.AddParameterWithValue ("@ISSUERNAME", issuerName); command.AddParameterWithValue ("@THISUPDATE", crl.ThisUpdate.ToUniversalTime ()); @@ -619,7 +704,7 @@ protected override DbCommand GetSelectAllCrlsCommand (DbConnection connection) { var command = connection.CreateCommand (); - command.CommandText = "SELECT ID, CRL FROM CRLS"; + command.CommandText = $"SELECT {CrlColumnNames.Id}, {CrlColumnNames.Crl} FROM {CrlsTableName}"; command.CommandType = CommandType.Text; return command; @@ -638,7 +723,7 @@ protected override DbCommand GetDeleteCommand (DbConnection connection, X509Cert { var command = connection.CreateCommand (); - command.CommandText = "DELETE FROM CERTIFICATES WHERE ID = @ID"; + command.CommandText = $"DELETE FROM {CertificatesTableName} WHERE {CertificateColumnNames.Id} = @ID"; command.AddParameterWithValue ("@ID", record.Id); command.CommandType = CommandType.Text; @@ -658,7 +743,7 @@ protected override DbCommand GetDeleteCommand (DbConnection connection, X509CrlR { var command = connection.CreateCommand (); - command.CommandText = "DELETE FROM CRLS WHERE ID = @ID"; + command.CommandText = $"DELETE FROM {CrlsTableName} WHERE {CrlColumnNames.Id} = @ID"; command.AddParameterWithValue ("@ID", record.Id); command.CommandType = CommandType.Text; @@ -676,7 +761,7 @@ protected override DbCommand GetDeleteCommand (DbConnection connection, X509CrlR /// The certificate record. protected override DbCommand GetInsertCommand (DbConnection connection, X509CertificateRecord record) { - var statement = new StringBuilder ("INSERT INTO CERTIFICATES("); + var statement = new StringBuilder ("INSERT INTO ").Append (CertificatesTableName).Append ('('); var variables = new StringBuilder ("VALUES("); var command = connection.CreateCommand (); var columns = CertificatesTable.Columns; @@ -715,7 +800,7 @@ protected override DbCommand GetInsertCommand (DbConnection connection, X509Cert /// The CRL record. protected override DbCommand GetInsertCommand (DbConnection connection, X509CrlRecord record) { - var statement = new StringBuilder ("INSERT INTO CRLS("); + var statement = new StringBuilder ("INSERT INTO ").Append (CrlsTableName).Append ('('); var variables = new StringBuilder ("VALUES("); var command = connection.CreateCommand (); var columns = CrlsTable.Columns; @@ -755,7 +840,7 @@ protected override DbCommand GetInsertCommand (DbConnection connection, X509CrlR /// The fields to update. protected override DbCommand GetUpdateCommand (DbConnection connection, X509CertificateRecord record, X509CertificateRecordFields fields) { - var statement = new StringBuilder ("UPDATE CERTIFICATES SET "); + var statement = new StringBuilder ("UPDATE ").Append (CertificatesTableName).Append (" SET "); var columns = GetColumnNames (fields & ~X509CertificateRecordFields.Id); var command = connection.CreateCommand (); @@ -773,7 +858,7 @@ protected override DbCommand GetUpdateCommand (DbConnection connection, X509Cert command.AddParameterWithValue (variable, value); } - statement.Append (" WHERE ID = @ID"); + statement.Append (" WHERE ").Append (CertificateColumnNames.Id).Append (" = @ID"); command.AddParameterWithValue ("@ID", record.Id); command.CommandText = statement.ToString (); @@ -793,7 +878,7 @@ protected override DbCommand GetUpdateCommand (DbConnection connection, X509Cert /// The CRL record. protected override DbCommand GetUpdateCommand (DbConnection connection, X509CrlRecord record) { - var statement = new StringBuilder ("UPDATE CRLS SET "); + var statement = new StringBuilder ("UPDATE ").Append (CrlsTableName).Append (" SET "); var command = connection.CreateCommand (); var columns = CrlsTable.Columns; @@ -811,7 +896,7 @@ protected override DbCommand GetUpdateCommand (DbConnection connection, X509CrlR command.AddParameterWithValue (variable, value); } - statement.Append (" WHERE ID = @ID"); + statement.Append (" WHERE ").Append (CrlColumnNames.Id).Append (" = @ID"); command.AddParameterWithValue ("@ID", record.Id); command.CommandText = statement.ToString (); diff --git a/MimeKit/Cryptography/TemporarySecureMimeContext.cs b/MimeKit/Cryptography/TemporarySecureMimeContext.cs index cc673a26c7..ef94a32466 100644 --- a/MimeKit/Cryptography/TemporarySecureMimeContext.cs +++ b/MimeKit/Cryptography/TemporarySecureMimeContext.cs @@ -26,6 +26,7 @@ using System; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; @@ -259,7 +260,10 @@ protected override DateTime GetNextCertificateRevocationListUpdate (X509Name iss X509Certificate GetCmsRecipientCertificate (MailboxAddress mailbox) { + var mailboxDomain = MailboxAddress.IdnMapping.Encode (mailbox.Domain); + var mailboxAddress = mailbox.GetAddress (true); var secure = mailbox as SecureMailboxAddress; + X509Certificate domainCertificate = null; var now = DateTime.UtcNow; foreach (var certificate in certificates) { @@ -276,16 +280,27 @@ X509Certificate GetCmsRecipientCertificate (MailboxAddress mailbox) if (!fingerprint.Equals (secure.Fingerprint, StringComparison.OrdinalIgnoreCase)) continue; } else { - var address = certificate.GetSubjectEmailAddress (); + var emailAddress = certificate.GetSubjectEmailAddress (true); + + if (!emailAddress.Equals (mailboxAddress, StringComparison.OrdinalIgnoreCase)) { + // Fall back to matching the domain... + if (domainCertificate == null) { + var domains = certificate.GetSubjectDnsNames (true); + + if (domains.Any (domain => domain.Equals (mailboxDomain, StringComparison.OrdinalIgnoreCase))) { + // Cache this certificate. We will only use this if we do not find an exact match based on the full email address. + domainCertificate = certificate; + } + } - if (!address.Equals (mailbox.Address, StringComparison.OrdinalIgnoreCase)) continue; + } } return certificate; } - return null; + return domainCertificate; } /// @@ -320,7 +335,11 @@ protected override CmsRecipient GetCmsRecipient (MailboxAddress mailbox) X509Certificate GetCmsSignerCertificate (MailboxAddress mailbox, out AsymmetricKeyParameter key) { + var mailboxDomain = MailboxAddress.IdnMapping.Encode (mailbox.Domain); + var mailboxAddress = mailbox.GetAddress (true); var secure = mailbox as SecureMailboxAddress; + X509Certificate domainCertificate = null; + AsymmetricKeyParameter domainKey = null; var now = DateTime.UtcNow; foreach (var certificate in certificates) { @@ -340,18 +359,30 @@ X509Certificate GetCmsSignerCertificate (MailboxAddress mailbox, out AsymmetricK if (!fingerprint.Equals (secure.Fingerprint, StringComparison.OrdinalIgnoreCase)) continue; } else { - var address = certificate.GetSubjectEmailAddress (); + var address = certificate.GetSubjectEmailAddress (true); + + if (!address.Equals (mailboxAddress, StringComparison.OrdinalIgnoreCase)) { + // Fall back to matching the domain... + if (domainCertificate == null) { + var domains = certificate.GetSubjectDnsNames (true); + + if (domains.Any (domain => domain.Equals (mailboxDomain, StringComparison.OrdinalIgnoreCase))) { + // Cache this certificate. We will only use this if we do not find an exact match based on the full email address. + domainCertificate = certificate; + domainKey = key; + } + } - if (!address.Equals (mailbox.Address, StringComparison.OrdinalIgnoreCase)) continue; + } } return certificate; } - key = null; + key = domainKey; - return null; + return domainCertificate; } /// diff --git a/MimeKit/Cryptography/WindowsSecureMimeContext.cs b/MimeKit/Cryptography/WindowsSecureMimeContext.cs index 1bdad9c31b..213833ec5b 100644 --- a/MimeKit/Cryptography/WindowsSecureMimeContext.cs +++ b/MimeKit/Cryptography/WindowsSecureMimeContext.cs @@ -26,6 +26,7 @@ using System; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; @@ -174,7 +175,10 @@ public override bool CanEncrypt (MailboxAddress mailbox, CancellationToken cance protected virtual X509Certificate2 GetRecipientCertificate (MailboxAddress mailbox) { var storeNames = new [] { StoreName.AddressBook, StoreName.My, StoreName.TrustedPeople }; + var mailboxDomain = MailboxAddress.IdnMapping.Encode (mailbox.Domain); + var mailboxAddress = mailbox.GetAddress (true); var secure = mailbox as SecureMailboxAddress; + X509Certificate2 domainCertificate = null; var now = DateTime.UtcNow; foreach (var storeName in storeNames) { @@ -197,8 +201,22 @@ protected virtual X509Certificate2 GetRecipientCertificate (MailboxAddress mailb } else { var address = certificate.GetNameInfo (X509NameType.EmailName, false); - if (!address.Equals (mailbox.Address, StringComparison.InvariantCultureIgnoreCase)) + if (!string.IsNullOrEmpty (address)) + address = MailboxAddress.EncodeAddrspec (address); + + if (!address.Equals (mailboxAddress, StringComparison.OrdinalIgnoreCase)) { + // Fall back to matching the domain... + if (domainCertificate == null) { + var domains = certificate.GetSubjectDnsNames (true); + + if (domains.Any (domain => domain.Equals (mailboxDomain, StringComparison.OrdinalIgnoreCase))) { + // Cache this certificate. We will only use this if we do not find an exact match based on the full email address. + domainCertificate = certificate; + } + } + continue; + } } return certificate; @@ -208,7 +226,7 @@ protected virtual X509Certificate2 GetRecipientCertificate (MailboxAddress mailb } } - return null; + return domainCertificate; } /// @@ -318,8 +336,11 @@ static RealCmsRecipientCollection GetCmsRecipients (CmsRecipientCollection recip /// The signer's mailbox address. protected virtual X509Certificate2 GetSignerCertificate (MailboxAddress mailbox) { + var mailboxDomain = MailboxAddress.IdnMapping.Encode (mailbox.Domain); + var mailboxAddress = mailbox.GetAddress (true); var store = new X509Store (StoreName.My, StoreLocation); var secure = mailbox as SecureMailboxAddress; + X509Certificate2 domainCertificate = null; var now = DateTime.UtcNow; store.Open (OpenFlags.ReadOnly); @@ -342,8 +363,22 @@ protected virtual X509Certificate2 GetSignerCertificate (MailboxAddress mailbox) } else { var address = certificate.GetNameInfo (X509NameType.EmailName, false); - if (!address.Equals (mailbox.Address, StringComparison.InvariantCultureIgnoreCase)) + if (!string.IsNullOrEmpty (address)) + address = MailboxAddress.EncodeAddrspec (address); + + if (!address.Equals (mailboxAddress, StringComparison.OrdinalIgnoreCase)) { + // Fall back to matching the domain... + if (domainCertificate == null) { + var domains = certificate.GetSubjectDnsNames (true); + + if (domains.Any (domain => domain.Equals (mailboxDomain, StringComparison.OrdinalIgnoreCase))) { + // Cache this certificate. We will only use this if we do not find an exact match based on the full email address. + domainCertificate = certificate; + } + } + continue; + } } return certificate; @@ -352,7 +387,7 @@ protected virtual X509Certificate2 GetSignerCertificate (MailboxAddress mailbox) store.Close (); } - return null; + return domainCertificate; } AsnEncodedData GetSecureMimeCapabilities () diff --git a/MimeKit/Cryptography/X509Certificate2Extensions.cs b/MimeKit/Cryptography/X509Certificate2Extensions.cs index d1a375af17..a07c42b7a9 100644 --- a/MimeKit/Cryptography/X509Certificate2Extensions.cs +++ b/MimeKit/Cryptography/X509Certificate2Extensions.cs @@ -37,6 +37,7 @@ using X509Certificate = Org.BouncyCastle.X509.X509Certificate; using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2; +using X509Extension = System.Security.Cryptography.X509Certificates.X509Extension; namespace MimeKit.Cryptography { /// @@ -101,6 +102,73 @@ public static PublicKeyAlgorithm GetPublicKeyAlgorithm (this X509Certificate2 ce } } + static string[] GetSubjectAlternativeNames (X509Certificate2 certificate, int tagNo) + { + X509Extension alt = null; + + foreach (var extension in certificate.Extensions) { + if (extension.Oid.Value == X509Extensions.SubjectAlternativeName.Id) { + alt = extension; + break; + } + } + + if (alt == null) + return Array.Empty (); + + using (var memory = new MemoryStream (alt.RawData, false)) { + var seq = Asn1Sequence.GetInstance (Asn1Object.FromByteArray (alt.RawData)); + 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 (); + + if (count < names.Length) + Array.Resize (ref names, count); + + return names; + } + } + + /// + /// Get the subject domain names of the certificate. + /// + /// + /// Gets the subject DNS names of the certificate. + /// Some S/MIME certificates are domain-bound instead of being bound to a + /// particular email address. + /// + /// The subject DNS names. + /// The certificate. + /// If set to true, international domain names will be IDN encoded. + /// + /// is . + /// + public static string[] GetSubjectDnsNames (this X509Certificate2 certificate, bool idnEncode = false) + { + if (certificate == null) + throw new ArgumentNullException (nameof (certificate)); + + 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 domains; + } + static EncryptionAlgorithm[] DecodeEncryptionAlgorithms (byte[] rawData) { using (var memory = new MemoryStream (rawData, false)) { diff --git a/MimeKit/Cryptography/X509CertificateDatabase.cs b/MimeKit/Cryptography/X509CertificateDatabase.cs index e335ee9800..08de0397e7 100644 --- a/MimeKit/Cryptography/X509CertificateDatabase.cs +++ b/MimeKit/Cryptography/X509CertificateDatabase.cs @@ -60,6 +60,254 @@ public abstract class X509CertificateDatabase : IX509CertificateDatabase DbConnection connection; char[] password; + /// + /// The name of the database table containing the certificates. + /// + /// + /// The name of the database table containing the certificates. + /// + protected const string CertificatesTableName = "CERTIFICATES"; + + /// + /// The name of the database table containing the CRLs. + /// + /// + /// The name of the database table containing the CRLs. + /// + protected const string CrlsTableName = "CRLS"; + + /// + /// The column names for the certificates table. + /// + /// + /// The column names for the certificates table. + /// + protected class CertificateColumnNames + { + /// + /// The auto-increment primary key identifier. + /// + /// + /// The auto-increment primary key identifier. + /// + public const string Id = "ID"; + + /// + /// A column specifying whether the certificate is trusted or not. + /// + /// + /// A column specifying whether the certificate is trusted or not. + /// This data-type for this column should be . + /// + public const string Trusted = "TRUSTED"; + + /// + /// A column specifying whether the certificate is an anchor. + /// + /// + /// A column specifying whether the certificate is an anchor. + /// This data-type for this column should be . + /// + public const string Anchor = "ANCHOR"; + + /// + /// A column specifying the basic constraints of the certificate. + /// + /// + /// A column specifying the basic constraints of the certificate. + /// This data-type for this column should be . + /// + public const string BasicConstraints = "BASICCONSTRAINTS"; + + /// + /// A column specifying the key usage of the certificate. + /// + /// + /// A column specifying the key usage of the certificate. + /// This data-type for this column should be . + /// + public const string KeyUsage = "KEYUSAGE"; + + /// + /// A column specifying the date and time when the certificate first becomes valid. + /// + /// + /// A column specifying the date and time when the certificate first becomes valid. + /// This data-type for this column should be . + /// + public const string NotBefore = "NOTBEFORE"; + + /// + /// A column specifying the date and time after which the certificate becomes invalid. + /// + /// + /// A column specifying the date and time after which the certificate becomes invalid. + /// This data-type for this column should be . + /// + public const string NotAfter = "NOTAFTER"; + + /// + /// A column specifying the issuer name of the certificate. + /// + /// + /// A column specifying the issuer name of the certificate. + /// This data-type for this column should be . + /// + public const string IssuerName = "ISSUERNAME"; + + /// + /// A column specifying the serial number of the certificate. + /// + /// + /// A column specifying the serial number of the certificate. + /// This data-type for this column should be . + /// + public const string SerialNumber = "SERIALNUMBER"; + + /// + /// A column specifying the subject name of the certificate. + /// + /// + /// A column specifying the subject name of the certificate. + /// This data-type for this column should be . + /// + public const string SubjectName = "SUBJECTNAME"; + + /// + /// A column specifying the subject key identifier of the certificate. + /// + /// + /// A column specifying the subject key identifier of the certificate. + /// This data-type for this column should be . + /// + public const string SubjectKeyIdentifier = "SUBJECTKEYIDENTIFIER"; + + /// + /// A column specifying the subject email address of the certificate. + /// + /// + /// A column specifying the subject email address of the certificate. + /// This data-type for this column should be . + /// + public const string SubjectEmail = "SUBJECTEMAIL"; + + /// + /// A column specifying the subject DNS names of the certificate. + /// + /// + /// A column specifying the subject DNS names of the certificate. + /// This data-type for this column should be . + /// + public const string SubjectDnsNames = "SUBJECTDNSNAMES"; + + /// + /// A column specifying the fingerprint of the certificate. + /// + /// + /// A column specifying the fingerprint of the certificate. + /// This data-type for this column should be . + /// + public const string Fingerprint = "FINGERPRINT"; + + /// + /// A column specifying the encryption algorithms supported by the certificate. + /// + /// + /// A column specifying the encryption algorithms supported by the certificate. + /// This data-type for this column should be . + /// + public const string Algorithms = "ALGORITHMS"; + + /// + /// A column specifying the date and time of the last update to the column. + /// + /// + /// A column specifying the date and time of the last update to the column. + /// This data-type for this column should be . + /// + public const string AlgorithmsUpdated = "ALGORITHMSUPDATED"; + + /// + /// A column containing the raw certificate data. + /// + /// + /// A column containing the raw certificate data. + /// This data-type for this column should be . + /// + public const string Certificate = "CERTIFICATE"; + + /// + /// A column containing the raw private key data. + /// + /// + /// A column containing the raw private key data. + /// This data-type for this column should be . + /// + public const string PrivateKey = "PRIVATEKEY"; + } + + /// + /// The column names for the CRLs table. + /// + /// + /// The column names for the CRLs table. + /// + protected class CrlColumnNames + { + /// + /// The auto-increment primary key identifier. + /// + /// + /// The auto-increment primary key identifier. + /// + public const string Id = "ID"; + + /// + /// A column specifying whether the CRL data is a delta update. + /// + /// + /// A column specifying whether the CRL data is a delta update. + /// This data-type for this column should be . + /// + public const string Delta = "DELTA"; + + /// + /// A column specifying the issuer name of the certificate. + /// + /// + /// A column specifying the issuer name of the certificate. + /// This data-type for this column should be . + /// + public const string IssuerName = "ISSUERNAME"; + + /// + /// A column specifying the date and time of the last update. + /// + /// + /// A column specifying the date and time of the last update. + /// This data-type for this column should be . + /// + public const string ThisUpdate = "THISUPDATE"; + + /// + /// A column specifying the date and time of the next update. + /// + /// + /// A column specifying the date and time of the next update. + /// This data-type for this column should be . + /// + public const string NextUpdate = "NEXTUPDATE"; + + /// + /// A column containing the raw CRL data. + /// + /// + /// A column containing the raw CRL data. + /// This data-type for this column should be . + /// + public const string Crl = "CRL"; + } + /// /// Initialize a new instance of the class. /// @@ -172,6 +420,26 @@ protected int SaltSize { get; set; } + internal static string EncodeDnsNames (string[] dnsNames) + { + if (dnsNames.Length == 0) + return string.Empty; + + int size = 1; + + for (int i = 0; i < dnsNames.Length; i++) + size += dnsNames[i].Length + 1; + + var encoded = new ValueStringBuilder (size); + encoded.Append ('|'); + for (int i = 0; i < dnsNames.Length; i++) { + encoded.Append (dnsNames[i]); + encoded.Append ('|'); + } + + return encoded.ToString (); + } + static int ReadBinaryBlob (DbDataReader reader, int column, ref byte[] buffer) { long nread; @@ -311,22 +579,22 @@ X509CertificateRecord LoadCertificateRecord (DbDataReader reader, X509Certificat for (int i = 0; i < reader.FieldCount; i++) { switch (reader.GetName (i).ToUpperInvariant ()) { - case "CERTIFICATE": + case CertificateColumnNames.Certificate: record.Certificate = DecodeCertificate (reader, parser, i, ref buffer); break; - case "PRIVATEKEY": + case CertificateColumnNames.PrivateKey: record.PrivateKey = DecodePrivateKey (reader, i, ref buffer); break; - case "ALGORITHMS": + case CertificateColumnNames.Algorithms: record.Algorithms = DecodeEncryptionAlgorithms (reader, i); break; - case "ALGORITHMSUPDATED": + case CertificateColumnNames.AlgorithmsUpdated: record.AlgorithmsUpdated = DateTime.SpecifyKind (reader.GetDateTime (i), DateTimeKind.Utc); break; - case "TRUSTED": + case CertificateColumnNames.Trusted: record.IsTrusted = reader.GetBoolean (i); break; - case "ID": + case CertificateColumnNames.Id: record.Id = reader.GetInt32 (i); break; } @@ -341,19 +609,19 @@ static X509CrlRecord LoadCrlRecord (DbDataReader reader, X509CrlParser parser, r for (int i = 0; i < reader.FieldCount; i++) { switch (reader.GetName (i).ToUpperInvariant ()) { - case "CRL": + case CrlColumnNames.Crl: record.Crl = DecodeX509Crl (reader, parser, i, ref buffer); break; - case "THISUPDATE": + case CrlColumnNames.ThisUpdate: record.ThisUpdate = DateTime.SpecifyKind (reader.GetDateTime (i), DateTimeKind.Utc); break; - case "NEXTUPDATE": + case CrlColumnNames.NextUpdate: record.NextUpdate = DateTime.SpecifyKind (reader.GetDateTime (i), DateTimeKind.Utc); break; - case "DELTA": + case CrlColumnNames.Delta: record.IsDelta = reader.GetBoolean (i); break; - case "ID": + case CrlColumnNames.Id: record.Id = reader.GetInt32 (i); break; } @@ -375,17 +643,17 @@ protected static string[] GetColumnNames (X509CertificateRecordFields fields) var columns = new List (); if ((fields & X509CertificateRecordFields.Id) != 0) - columns.Add ("ID"); + columns.Add (CertificateColumnNames.Id); if ((fields & X509CertificateRecordFields.Trusted) != 0) - columns.Add ("TRUSTED"); + columns.Add (CertificateColumnNames.Trusted); if ((fields & X509CertificateRecordFields.Algorithms) != 0) - columns.Add ("ALGORITHMS"); + columns.Add (CertificateColumnNames.Algorithms); if ((fields & X509CertificateRecordFields.AlgorithmsUpdated) != 0) - columns.Add ("ALGORITHMSUPDATED"); + columns.Add (CertificateColumnNames.AlgorithmsUpdated); if ((fields & X509CertificateRecordFields.Certificate) != 0) - columns.Add ("CERTIFICATE"); + columns.Add (CertificateColumnNames.Certificate); if ((fields & X509CertificateRecordFields.PrivateKey) != 0) - columns.Add ("PRIVATEKEY"); + columns.Add (CertificateColumnNames.PrivateKey); return columns.ToArray (); } @@ -443,17 +711,17 @@ protected static string[] GetColumnNames (X509CrlRecordFields fields) var columns = new List (); if ((fields & X509CrlRecordFields.Id) != 0) - columns.Add ("ID"); + columns.Add (CrlColumnNames.Id); if ((fields & X509CrlRecordFields.IsDelta) != 0) - columns.Add ("DELTA"); + columns.Add (CrlColumnNames.Delta); if ((fields & X509CrlRecordFields.IssuerName) != 0) - columns.Add ("ISSUERNAME"); + columns.Add (CrlColumnNames.IssuerName); if ((fields & X509CrlRecordFields.ThisUpdate) != 0) - columns.Add ("THISUPDATE"); + columns.Add (CrlColumnNames.ThisUpdate); if ((fields & X509CrlRecordFields.NextUpdate) != 0) - columns.Add ("NEXTUPDATE"); + columns.Add (CrlColumnNames.NextUpdate); if ((fields & X509CrlRecordFields.Crl) != 0) - columns.Add ("CRL"); + columns.Add (CrlColumnNames.Crl); return columns.ToArray (); } @@ -529,23 +797,24 @@ protected static string[] GetColumnNames (X509CrlRecordFields fields) protected object GetValue (X509CertificateRecord record, string columnName) { switch (columnName) { - //case "ID": return record.Id; - case "BASICCONSTRAINTS": return record.BasicConstraints; - case "TRUSTED": return record.IsTrusted; - case "ANCHOR": return record.IsAnchor; - case "KEYUSAGE": return (int) record.KeyUsage; - case "NOTBEFORE": return record.NotBefore.ToUniversalTime (); - case "NOTAFTER": return record.NotAfter.ToUniversalTime (); - case "ISSUERNAME": return record.IssuerName; - case "SERIALNUMBER": return record.SerialNumber; - case "SUBJECTNAME": return record.SubjectName; - case "SUBJECTKEYIDENTIFIER": return record.SubjectKeyIdentifier?.AsHex (); - case "SUBJECTEMAIL": return record.SubjectEmail != null ? record.SubjectEmail.ToLowerInvariant () : string.Empty; - case "FINGERPRINT": return record.Fingerprint.ToLowerInvariant (); - case "ALGORITHMS": return EncodeEncryptionAlgorithms (record.Algorithms); - case "ALGORITHMSUPDATED": return record.AlgorithmsUpdated; - case "CERTIFICATE": return record.Certificate.GetEncoded (); - case "PRIVATEKEY": return EncodePrivateKey (record.PrivateKey); + //case CertificateColumnNames.Id: return record.Id; + case CertificateColumnNames.BasicConstraints: return record.BasicConstraints; + case CertificateColumnNames.Trusted: return record.IsTrusted; + case CertificateColumnNames.Anchor: return record.IsAnchor; + case CertificateColumnNames.KeyUsage: return (int) record.KeyUsage; + case CertificateColumnNames.NotBefore: return record.NotBefore.ToUniversalTime (); + case CertificateColumnNames.NotAfter: return record.NotAfter.ToUniversalTime (); + case CertificateColumnNames.IssuerName: return record.IssuerName; + case CertificateColumnNames.SerialNumber: return record.SerialNumber; + case CertificateColumnNames.SubjectName: return record.SubjectName; + case CertificateColumnNames.SubjectKeyIdentifier: return record.SubjectKeyIdentifier?.AsHex (); + case CertificateColumnNames.SubjectEmail: return record.SubjectEmail; + case CertificateColumnNames.SubjectDnsNames: return EncodeDnsNames (record.SubjectDnsNames); + case CertificateColumnNames.Fingerprint: return record.Fingerprint.ToLowerInvariant (); + case CertificateColumnNames.Algorithms: return EncodeEncryptionAlgorithms (record.Algorithms); + case CertificateColumnNames.AlgorithmsUpdated: return record.AlgorithmsUpdated; + case CertificateColumnNames.Certificate: return record.Certificate.GetEncoded (); + case CertificateColumnNames.PrivateKey: return EncodePrivateKey (record.PrivateKey); default: throw new ArgumentException (string.Format ("Unknown column name: {0}", columnName), nameof (columnName)); } } @@ -565,12 +834,12 @@ protected object GetValue (X509CertificateRecord record, string columnName) protected static object GetValue (X509CrlRecord record, string columnName) { switch (columnName) { - //case "ID": return record.Id; - case "DELTA": return record.IsDelta; - case "ISSUERNAME": return record.IssuerName; - case "THISUPDATE": return record.ThisUpdate; - case "NEXTUPDATE": return record.NextUpdate; - case "CRL": return record.Crl.GetEncoded (); + //case CrlColumnNames.Id: return record.Id; + case CrlColumnNames.Delta: return record.IsDelta; + case CrlColumnNames.IssuerName: return record.IssuerName; + case CrlColumnNames.ThisUpdate: return record.ThisUpdate; + case CrlColumnNames.NextUpdate: return record.NextUpdate; + case CrlColumnNames.Crl: return record.Crl.GetEncoded (); default: throw new ArgumentException (string.Format ("Unknown column name: {0}", columnName), nameof (columnName)); } } diff --git a/MimeKit/Cryptography/X509CertificateRecord.cs b/MimeKit/Cryptography/X509CertificateRecord.cs index 4d3f637a27..8af75ef0b1 100644 --- a/MimeKit/Cryptography/X509CertificateRecord.cs +++ b/MimeKit/Cryptography/X509CertificateRecord.cs @@ -202,7 +202,25 @@ public byte[] SubjectKeyIdentifier { /// Gets the subject email address. /// /// The subject email address. - public string SubjectEmail { get { return Certificate.GetSubjectEmailAddress (); } } + public string SubjectEmail { get { return Certificate.GetSubjectEmailAddress (true).ToLowerInvariant (); } } + + /// + /// Gets the subject DNS names. + /// + /// + /// Gets the subject DNS names. + /// + /// The subject DNS names. + public string[] SubjectDnsNames { + get { + var domains = Certificate.GetSubjectDnsNames (true); + + for (int i = 0; i < domains.Length; i++) + domains[i] = domains[i].ToLowerInvariant (); + + return domains; + } + } /// /// Gets the fingerprint of the certificate. diff --git a/UnitTests/Cryptography/ApplicationPkcs7MimeTests.cs b/UnitTests/Cryptography/ApplicationPkcs7MimeTests.cs index d2f8aca6a3..719a0d5f86 100644 --- a/UnitTests/Cryptography/ApplicationPkcs7MimeTests.cs +++ b/UnitTests/Cryptography/ApplicationPkcs7MimeTests.cs @@ -49,7 +49,7 @@ protected virtual EncryptionAlgorithm[] GetEncryptionAlgorithms (IDigitalSignatu [Test] public void TestArgumentExceptions () { - var rsa = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = SecureMimeTestsBase.RsaCertificate; var entity = new TextPart ("plain") { Text = "This is some text..." }; var mailbox = new MailboxAddress ("MimeKit UnitTests", rsa.EmailAddress); var signer = new CmsSigner (rsa.FileName, "no.secret"); @@ -406,6 +406,9 @@ public async Task TestEncryptCmsRecipientsAsync (SubjectIdentifierType recipient public void TestEncryptMailboxes () { foreach (var certificate in SecureMimeTestsBase.SupportedCertificates) { + if (string.IsNullOrEmpty (certificate.EmailAddress)) + continue; + var entity = new TextPart ("plain") { Text = "This is some text..." }; var mailbox = new MailboxAddress ("MimeKit UnitTests", certificate.EmailAddress); var mailboxes = new[] { mailbox }; @@ -436,6 +439,9 @@ public void TestEncryptMailboxes () public async Task TestEncryptMailboxesAsync () { foreach (var certificate in SecureMimeTestsBase.SupportedCertificates) { + if (string.IsNullOrEmpty (certificate.EmailAddress)) + continue; + var entity = new TextPart ("plain") { Text = "This is some text..." }; var mailbox = new MailboxAddress ("MimeKit UnitTests", certificate.EmailAddress); var mailboxes = new[] { mailbox }; @@ -462,6 +468,70 @@ public async Task TestEncryptMailboxesAsync () } } + [Test] + public void TestEncryptDnsNames () + { + var certificate = SecureMimeTestsBase.DomainCertificate; + + foreach (var domain in certificate.DnsNames) { + var entity = new TextPart ("plain") { Text = "This is some text..." }; + var mailbox = new MailboxAddress ("MimeKit UnitTests", "mimekit@" + domain); + var mailboxes = new[] { mailbox }; + + using (var ctx = CreateContext ()) { + ApplicationPkcs7Mime encrypted; + MimeEntity decrypted; + TextPart text; + + ctx.Import (certificate.FileName, "no.secret"); + + encrypted = ApplicationPkcs7Mime.Encrypt (mailboxes, entity); + decrypted = encrypted.Decrypt (ctx); + Assert.That (decrypted, Is.InstanceOf (), "Decrypted from Encrypt(mailboxes, entity)"); + text = (TextPart) decrypted; + Assert.That (text.Text, Is.EqualTo (entity.Text), "Decrypted text"); + + encrypted = ApplicationPkcs7Mime.Encrypt (ctx, mailboxes, entity); + decrypted = encrypted.Decrypt (ctx); + Assert.That (decrypted, Is.InstanceOf (), "Encrypt(ctx, mailboxes, entity)"); + text = (TextPart) decrypted; + Assert.That (text.Text, Is.EqualTo (entity.Text), "Decrypted text"); + } + } + } + + [Test] + public async Task TestEncryptDnsNamesAsync () + { + var certificate = SecureMimeTestsBase.DomainCertificate; + + foreach (var domain in certificate.DnsNames) { + var entity = new TextPart ("plain") { Text = "This is some text..." }; + var mailbox = new MailboxAddress ("MimeKit UnitTests", "mimekit@" + domain); + var mailboxes = new[] { mailbox }; + + using (var ctx = CreateContext ()) { + ApplicationPkcs7Mime encrypted; + MimeEntity decrypted; + TextPart text; + + await ctx.ImportAsync (certificate.FileName, "no.secret").ConfigureAwait (false); + + encrypted = await ApplicationPkcs7Mime.EncryptAsync (mailboxes, entity).ConfigureAwait (false); + decrypted = await encrypted.DecryptAsync (ctx).ConfigureAwait (false); + Assert.That (decrypted, Is.InstanceOf (), "Decrypted from EncryptAsync(mailboxes, entity)"); + text = (TextPart) decrypted; + Assert.That (text.Text, Is.EqualTo (entity.Text), "Decrypted text"); + + encrypted = await ApplicationPkcs7Mime.EncryptAsync (ctx, mailboxes, entity).ConfigureAwait (false); + decrypted = await encrypted.DecryptAsync (ctx).ConfigureAwait (false); + Assert.That (decrypted, Is.InstanceOf (), "EncryptAsync(ctx, mailboxes, entity)"); + text = (TextPart) decrypted; + Assert.That (text.Text, Is.EqualTo (entity.Text), "Decrypted text"); + } + } + } + void AssertSignResults (SMimeCertificate certificate, SecureMimeContext ctx, ApplicationPkcs7Mime signed, TextPart entity) { var signatures = signed.Verify (ctx, out var encapsulated); @@ -541,6 +611,9 @@ public void TestSignMailbox () ImportAll (ctx); foreach (var certificate in SecureMimeTestsBase.SupportedCertificates) { + if (string.IsNullOrEmpty (certificate.EmailAddress)) + continue; + var mailbox = new SecureMailboxAddress ("MimeKit UnitTests", certificate.EmailAddress, certificate.Fingerprint); var entity = new TextPart ("plain") { Text = "This is some text..." }; @@ -557,6 +630,9 @@ public async Task TestSignMailboxAsync () await ImportAllAsync (ctx).ConfigureAwait (false); foreach (var certificate in SecureMimeTestsBase.SupportedCertificates) { + if (string.IsNullOrEmpty (certificate.EmailAddress)) + continue; + var mailbox = new SecureMailboxAddress ("MimeKit UnitTests", certificate.EmailAddress, certificate.Fingerprint); var entity = new TextPart ("plain") { Text = "This is some text..." }; @@ -566,6 +642,42 @@ public async Task TestSignMailboxAsync () } } + [Test] + public void TestSignDnsNames () + { + var certificate = SecureMimeTestsBase.DomainCertificate; + + using (var ctx = CreateContext ()) { + ImportAll (ctx); + + foreach (var domain in certificate.DnsNames) { + var mailbox = new MailboxAddress ("MimeKit UnitTests", "mimekit@" + domain); + var entity = new TextPart ("plain") { Text = "This is some text..." }; + + var signed = ApplicationPkcs7Mime.Sign (ctx, mailbox, DigestAlgorithm.Sha224, entity); + AssertSignResults (certificate, ctx, signed, entity); + } + } + } + + [Test] + public async Task TestSignDnsNamesAsync () + { + var certificate = SecureMimeTestsBase.DomainCertificate; + + using (var ctx = CreateContext ()) { + await ImportAllAsync (ctx).ConfigureAwait (false); + + foreach (var domain in certificate.DnsNames) { + var mailbox = new MailboxAddress ("MimeKit UnitTests", "mimekit@" + domain); + var entity = new TextPart ("plain") { Text = "This is some text..." }; + + var signed = await ApplicationPkcs7Mime.SignAsync (ctx, mailbox, DigestAlgorithm.Sha224, entity).ConfigureAwait (false); + AssertSignResults (certificate, ctx, signed, entity); + } + } + } + void AssertSignAndEncryptResults (SMimeCertificate certificate, SecureMimeContext ctx, ApplicationPkcs7Mime encrypted, TextPart entity) { var decrypted = encrypted.Decrypt (ctx); @@ -709,6 +821,9 @@ public void TestSignAndEncryptMailboxes () ImportAll (ctx); foreach (var certificate in SecureMimeTestsBase.SupportedCertificates) { + if (string.IsNullOrEmpty (certificate.EmailAddress)) + continue; + var mailbox = new SecureMailboxAddress ("MimeKit UnitTests", certificate.EmailAddress, certificate.Fingerprint); var entity = new TextPart ("plain") { Text = "This is some text..." }; var recipients = new MailboxAddress[] { mailbox }; @@ -726,6 +841,9 @@ public async Task TestSignAndEncryptMailboxesAsync () await ImportAllAsync (ctx).ConfigureAwait (false); foreach (var certificate in SecureMimeTestsBase.SupportedCertificates) { + if (string.IsNullOrEmpty (certificate.EmailAddress)) + continue; + var mailbox = new SecureMailboxAddress ("MimeKit UnitTests", certificate.EmailAddress, certificate.Fingerprint); var entity = new TextPart ("plain") { Text = "This is some text..." }; var recipients = new MailboxAddress[] { mailbox }; @@ -735,6 +853,44 @@ public async Task TestSignAndEncryptMailboxesAsync () } } } + + [Test] + public void TestSignAndEncryptDnsNames () + { + var certificate = SecureMimeTestsBase.DomainCertificate; + + using (var ctx = CreateContext ()) { + ImportAll (ctx); + + foreach (var domain in certificate.DnsNames) { + var mailbox = new MailboxAddress ("MimeKit UnitTests", "mimekit@" + domain); + var entity = new TextPart ("plain") { Text = "This is some text..." }; + var recipients = new MailboxAddress[] { mailbox }; + + var encrypted = ApplicationPkcs7Mime.SignAndEncrypt (mailbox, DigestAlgorithm.Sha224, recipients, entity); + AssertSignAndEncryptResults (certificate, ctx, encrypted, entity); + } + } + } + + [Test] + public async Task TestSignAndEncryptDnsNamesAsync () + { + var certificate = SecureMimeTestsBase.DomainCertificate; + + using (var ctx = CreateContext ()) { + await ImportAllAsync (ctx).ConfigureAwait (false); + + foreach (var domain in certificate.DnsNames) { + var mailbox = new MailboxAddress ("MimeKit UnitTests", "mimekit@" + domain); + var entity = new TextPart ("plain") { Text = "This is some text..." }; + var recipients = new MailboxAddress[] { mailbox }; + + var encrypted = await ApplicationPkcs7Mime.SignAndEncryptAsync (mailbox, DigestAlgorithm.Sha224, recipients, entity).ConfigureAwait (false); + await AssertSignAndEncryptResultsAsync (certificate, ctx, encrypted, entity).ConfigureAwait (false); + } + } + } } [TestFixture] diff --git a/UnitTests/Cryptography/CertificateExtensionTests.cs b/UnitTests/Cryptography/CertificateExtensionTests.cs index 1a5a5d3533..9866d1dd5c 100644 --- a/UnitTests/Cryptography/CertificateExtensionTests.cs +++ b/UnitTests/Cryptography/CertificateExtensionTests.cs @@ -91,5 +91,41 @@ public void TestCertificateConversion () } } } + + [Test] + public void TestGetSubjectDnsNames () + { + var certificate = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.DnsNames.Length > 0); + var path = Path.Combine (TestHelper.ProjectDir, "TestData", "smime", "dnsnames", "smime.pfx"); + var parser = new X509CertificateParser (); + + using (var stream = File.OpenRead (path)) { + var certificate2 = certificate.Certificate.AsX509Certificate2 (); + var certificate1 = certificate2.AsBouncyCastleCertificate (); + + Assert.That (certificate1.GetFingerprint ().ToUpperInvariant (), Is.EqualTo (certificate2.Thumbprint), "Fingerprint"); + Assert.That (certificate1.GetIssuerNameInfo (X509Name.EmailAddress), Is.EqualTo (certificate2.GetNameInfo (X509NameType.EmailName, true)), "Issuer Email"); + Assert.That (certificate1.GetSubjectEmailAddress (), Is.EqualTo (certificate2.GetNameInfo (X509NameType.EmailName, false)), "Subject Email"); + Assert.That (certificate1.GetCommonName (), Is.EqualTo (certificate2.GetNameInfo (X509NameType.SimpleName, false)), "Common Name"); + + var usage2 = GetX509Certificate2KeyUsageFlags (certificate2); + var usage1 = certificate1.GetKeyUsageFlags (); + + Assert.That (usage1, Is.EqualTo (usage2), "KeyUsageFlags"); + + var dnsNames = certificate1.GetSubjectDnsNames (); + var expectedDnsNames = certificate.DnsNames; + + Assert.That (dnsNames.Length, Is.EqualTo (expectedDnsNames.Length), "SubjectDnsNames.Length"); + for (int i = 0; i < dnsNames.Length; i++) + Assert.That (dnsNames[i], Is.EqualTo (expectedDnsNames[i]), $"SubjectDnsNames[{i}]"); + + dnsNames = certificate2.GetSubjectDnsNames (); + + Assert.That (dnsNames.Length, Is.EqualTo (expectedDnsNames.Length), "SubjectDnsNames.Length #2"); + for (int i = 0; i < dnsNames.Length; i++) + Assert.That (dnsNames[i], Is.EqualTo (expectedDnsNames[i]), $"SubjectDnsNames[{i}] #2"); + } + } } } diff --git a/UnitTests/Cryptography/CmsSignerTests.cs b/UnitTests/Cryptography/CmsSignerTests.cs index b09b659c45..32288795e5 100644 --- a/UnitTests/Cryptography/CmsSignerTests.cs +++ b/UnitTests/Cryptography/CmsSignerTests.cs @@ -43,7 +43,7 @@ public class CmsSignerTests [Test] public void TestArgumentExceptions () { - var rsa = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = SecureMimeTestsBase.RsaCertificate; var signer = new CmsSigner (rsa.FileName, "no.secret"); var certificate = new X509Certificate2 (signer.Certificate.GetEncoded ()); var chain = new[] { DotNetUtilities.FromX509Certificate (certificate) }; @@ -161,7 +161,7 @@ public void TestConstructors () [Test] public void TestDefaultValues () { - var rsa = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = SecureMimeTestsBase.RsaCertificate; List certificates; AsymmetricKeyParameter key; var password = "no.secret"; @@ -195,7 +195,7 @@ public void TestDefaultValues () [Test] public void TestSignerIdentifierType () { - var rsa = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = SecureMimeTestsBase.RsaCertificate; List certificates; AsymmetricKeyParameter key; var password = "no.secret"; @@ -229,7 +229,7 @@ public void TestSignerIdentifierType () [Test] public void TestRsaSignaturePadding () { - var rsa = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = SecureMimeTestsBase.RsaCertificate; var signer = new CmsSigner (rsa.FileName, "no.secret"); Assert.That (signer.RsaSignaturePadding, Is.Null, "Default RsaSignaturePadding"); diff --git a/UnitTests/Cryptography/DefaultSecureMimeContextTests.cs b/UnitTests/Cryptography/DefaultSecureMimeContextTests.cs index 5075093d5f..9b98d9ef87 100644 --- a/UnitTests/Cryptography/DefaultSecureMimeContextTests.cs +++ b/UnitTests/Cryptography/DefaultSecureMimeContextTests.cs @@ -145,7 +145,7 @@ public void TestImportCertificates () [Test] public void TestImportX509Certificate2 () { - var rsa = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = SecureMimeTestsBase.RsaCertificate; try { using (var ctx = new DefaultSecureMimeContext ("smime.db", "no.secret")) { @@ -170,7 +170,7 @@ public void TestImportX509Certificate2 () [Test] public async Task TestImportX509Certificate2Async () { - var rsa = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = SecureMimeTestsBase.RsaCertificate; try { using (var ctx = new DefaultSecureMimeContext ("smime.db", "no.secret")) { diff --git a/UnitTests/Cryptography/SecureMimeDigitalCertificateTests.cs b/UnitTests/Cryptography/SecureMimeDigitalCertificateTests.cs index b596ef450f..163ec4bcb1 100644 --- a/UnitTests/Cryptography/SecureMimeDigitalCertificateTests.cs +++ b/UnitTests/Cryptography/SecureMimeDigitalCertificateTests.cs @@ -39,7 +39,7 @@ public class SecureMimeDigitalCertificateTests [Test] public void TestArgumentExceptions () { - var rsa = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = SecureMimeTestsBase.RsaCertificate; var signer = new CmsSigner (rsa.FileName, "no.secret"); Assert.Throws (() => new SecureMimeDigitalCertificate (null)); diff --git a/UnitTests/Cryptography/SecureMimeTests.cs b/UnitTests/Cryptography/SecureMimeTests.cs index f28090d01a..f307d46858 100644 --- a/UnitTests/Cryptography/SecureMimeTests.cs +++ b/UnitTests/Cryptography/SecureMimeTests.cs @@ -45,6 +45,7 @@ public class SMimeCertificate public DateTime CreationDate { get { return Certificate.NotBefore; } } public DateTime ExpirationDate { get { return Certificate.NotAfter; } } public string EmailAddress { get { return Certificate.GetSubjectEmailAddress (); } } + public string[] DnsNames { get { return Certificate.GetSubjectDnsNames (); } } public string Fingerprint { get; private set; } public PublicKeyAlgorithm PublicKeyAlgorithm { get { return Certificate.GetPublicKeyAlgorithm (); } } @@ -77,6 +78,8 @@ public abstract class SecureMimeTestsBase public static readonly SMimeCertificate[] UnsupportedCertificates; public static readonly SMimeCertificate[] SupportedCertificates; public static readonly SMimeCertificate[] SMimeCertificates; + public static readonly SMimeCertificate DomainCertificate; + public static readonly SMimeCertificate RsaCertificate; protected virtual bool IsEnabled { get { return true; } } @@ -123,6 +126,13 @@ static SecureMimeTestsBase () break; } + if (smime.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral) { + if (!string.IsNullOrEmpty (smime.EmailAddress)) + RsaCertificate = smime; + else if (smime.DnsNames.Length > 0) + DomainCertificate = smime; + } + all.Add (smime); } continue; @@ -145,6 +155,13 @@ static SecureMimeTestsBase () break; } + if (smime.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral) { + if (!string.IsNullOrEmpty (smime.EmailAddress)) + RsaCertificate = smime; + else if (smime.DnsNames.Length > 0) + DomainCertificate = smime; + } + all.Add (smime); } } @@ -389,6 +406,9 @@ public virtual void TestCanSignAndEncrypt () { using (var ctx = CreateContext ()) { foreach (var certificate in SupportedCertificates) { + if (string.IsNullOrEmpty (certificate.EmailAddress)) + continue; + var valid = new MailboxAddress ("MimeKit UnitTests", certificate.EmailAddress); var invalid = new MailboxAddress ("Joe Nobody", "joe@nobody.com"); @@ -412,6 +432,9 @@ public virtual async Task TestCanSignAndEncryptAsync () { using (var ctx = CreateContext ()) { foreach (var certificate in SupportedCertificates) { + if (string.IsNullOrEmpty (certificate.EmailAddress)) + continue; + var valid = new MailboxAddress ("MimeKit UnitTests", certificate.EmailAddress); var invalid = new MailboxAddress ("Joe Nobody", "joe@nobody.com"); @@ -430,6 +453,56 @@ public virtual async Task TestCanSignAndEncryptAsync () } } + [Test] + public virtual void TestCanSignAndEncryptDnsNames () + { + var certificate = DomainCertificate; + + using (var ctx = CreateContext ()) { + foreach (var domain in certificate.DnsNames) { + var valid = new MailboxAddress ("MimeKit UnitTests", "mimekit@" + domain); + var invalid = new MailboxAddress ("Joe Nobody", "joe@nobody.com"); + + Assert.That (ctx.CanSign (invalid), Is.False, $"{invalid} should not be able to sign."); + Assert.That (ctx.CanEncrypt (invalid), Is.False, $"{invalid} should not be able to encrypt."); + + Assert.That (ctx.CanSign (valid), Is.True, $"{valid} should be able to sign."); + Assert.That (ctx.CanEncrypt (valid), Is.True, $"{valid} should be able to encrypt."); + + using (var content = new MemoryStream ()) { + Assert.Throws (() => ctx.Encrypt (new[] { invalid }, content)); + Assert.Throws (() => ctx.Sign (invalid, DigestAlgorithm.Sha1, content)); + Assert.Throws (() => ctx.EncapsulatedSign (invalid, DigestAlgorithm.Sha1, content)); + } + } + } + } + + [Test] + public virtual async Task TestCanSignAndEncryptDnsNamesAsync () + { + var certificate = DomainCertificate; + + using (var ctx = CreateContext ()) { + foreach (var domain in certificate.DnsNames) { + var valid = new MailboxAddress ("MimeKit UnitTests", "mimekit@" + domain); + var invalid = new MailboxAddress ("Joe Nobody", "joe@nobody.com"); + + Assert.That (await ctx.CanSignAsync (invalid), Is.False, $"{invalid} should not be able to sign."); + Assert.That (await ctx.CanEncryptAsync (invalid), Is.False, $"{invalid} should not be able to encrypt."); + + Assert.That (await ctx.CanSignAsync (valid), Is.True, $"{valid} should be able to sign."); + Assert.That (await ctx.CanEncryptAsync (valid), Is.True, $"{valid} should be able to encrypt."); + + using (var content = new MemoryStream ()) { + Assert.ThrowsAsync (() => ctx.EncryptAsync (new[] { invalid }, content)); + Assert.ThrowsAsync (() => ctx.SignAsync (invalid, DigestAlgorithm.Sha1, content)); + Assert.ThrowsAsync (() => ctx.EncapsulatedSignAsync (invalid, DigestAlgorithm.Sha1, content)); + } + } + } + } + [Test] public void TestDigestAlgorithmMappings () { @@ -575,6 +648,9 @@ public virtual void TestSecureMimeEncapsulatedSigning () if (Environment.OSVersion.Platform == PlatformID.Win32NT && certificate.PublicKeyAlgorithm == PublicKeyAlgorithm.EllipticCurve) continue; + if (string.IsNullOrEmpty (certificate.EmailAddress)) + continue; + var self = new MailboxAddress ("MimeKit UnitTests", certificate.EmailAddress); var signed = ApplicationPkcs7Mime.Sign (self, DigestAlgorithm.Sha1, cleartext); @@ -630,6 +706,9 @@ public virtual async Task TestSecureMimeEncapsulatedSigningAsync () if (Environment.OSVersion.Platform == PlatformID.Win32NT && certificate.PublicKeyAlgorithm == PublicKeyAlgorithm.EllipticCurve) continue; + if (string.IsNullOrEmpty (certificate.EmailAddress)) + continue; + var self = new MailboxAddress ("MimeKit UnitTests", certificate.EmailAddress); var signed = await ApplicationPkcs7Mime.SignAsync (self, DigestAlgorithm.Sha1, cleartext); MimeEntity extracted; @@ -726,7 +805,7 @@ public virtual void TestSecureMimeEncapsulatedSigningWithContext () using (var ctx = CreateContext ()) { foreach (var certificate in SupportedCertificates) { - if (!Supports (certificate.PublicKeyAlgorithm)) + if (!Supports (certificate.PublicKeyAlgorithm) || string.IsNullOrEmpty (certificate.EmailAddress)) continue; var self = new MailboxAddress ("MimeKit UnitTests", certificate.EmailAddress); @@ -764,7 +843,7 @@ public virtual async Task TestSecureMimeEncapsulatedSigningWithContextAsync () using (var ctx = CreateContext ()) { foreach (var certificate in SupportedCertificates) { - if (!Supports (certificate.PublicKeyAlgorithm)) + if (!Supports (certificate.PublicKeyAlgorithm) || string.IsNullOrEmpty (certificate.EmailAddress)) continue; var self = new MailboxAddress ("MimeKit UnitTests", certificate.EmailAddress); @@ -1337,7 +1416,7 @@ public virtual async Task TestSecureMimeSigningWithRsaSsaPssAsync () public virtual void TestSecureMimeMessageSigning () { foreach (var certificate in SupportedCertificates) { - if (!Supports (certificate.PublicKeyAlgorithm)) + if (!Supports (certificate.PublicKeyAlgorithm) || string.IsNullOrEmpty (certificate.EmailAddress)) continue; var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up signing..." }; @@ -1418,7 +1497,7 @@ public virtual void TestSecureMimeMessageSigning () public virtual async Task TestSecureMimeMessageSigningAsync () { foreach (var certificate in SupportedCertificates) { - if (!Supports (certificate.PublicKeyAlgorithm)) + if (!Supports (certificate.PublicKeyAlgorithm) || string.IsNullOrEmpty (certificate.EmailAddress)) continue; var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up signing..." }; @@ -1616,7 +1695,7 @@ public virtual void TestSecureMimeMessageEncryption () { using (var ctx = CreateContext ()) { foreach (var certificate in SupportedCertificates) { - if (!Supports (certificate.PublicKeyAlgorithm)) + if (!Supports (certificate.PublicKeyAlgorithm) || string.IsNullOrEmpty (certificate.EmailAddress)) continue; var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up encrypting..." }; @@ -1649,7 +1728,7 @@ public virtual async Task TestSecureMimeMessageEncryptionAsync () { using (var ctx = CreateContext ()) { foreach (var certificate in SupportedCertificates) { - if (!Supports (certificate.PublicKeyAlgorithm)) + if (!Supports (certificate.PublicKeyAlgorithm) || string.IsNullOrEmpty (certificate.EmailAddress)) continue; var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up encrypting..." }; @@ -1999,7 +2078,7 @@ public virtual async Task TestSecureMimeEncryptionWithAlgorithmAsync (SubjectIde [TestCase (DigestAlgorithm.Sha512)] public virtual void TestSecureMimeEncryptionWithRsaesOaep (DigestAlgorithm hashAlgorithm) { - var rsa = SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = RsaCertificate; var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up encrypting..." }; using (var ctx = CreateContext ()) { @@ -2041,7 +2120,7 @@ public virtual void TestSecureMimeEncryptionWithRsaesOaep (DigestAlgorithm hashA [TestCase (DigestAlgorithm.Sha512)] public virtual async Task TestSecureMimeEncryptionWithRsaesOaepAsync (DigestAlgorithm hashAlgorithm) { - var rsa = SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = RsaCertificate; var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up encrypting..." }; using (var ctx = CreateContext ()) { @@ -2161,7 +2240,7 @@ public async Task TestSecureMimeDecryptThunderbirdAsync () public virtual void TestSecureMimeSignAndEncrypt () { foreach (var certificate in SupportedCertificates) { - if (!Supports (certificate.PublicKeyAlgorithm)) + if (!Supports (certificate.PublicKeyAlgorithm) || string.IsNullOrEmpty (certificate.EmailAddress)) continue; var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up signing and encrypting..." }; @@ -2251,7 +2330,7 @@ public virtual void TestSecureMimeSignAndEncrypt () public virtual async Task TestSecureMimeSignAndEncryptAsync () { foreach (var certificate in SupportedCertificates) { - if (!Supports (certificate.PublicKeyAlgorithm)) + if (!Supports (certificate.PublicKeyAlgorithm) || string.IsNullOrEmpty (certificate.EmailAddress)) continue; var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up signing and encrypting..." }; @@ -2715,6 +2794,24 @@ public override Task TestCanSignAndEncryptAsync () return base.TestCanSignAndEncryptAsync (); } + [Test] + public override void TestCanSignAndEncryptDnsNames () + { + if (!IsEnabled) + return; + + base.TestCanSignAndEncryptDnsNames (); + } + + [Test] + public override Task TestCanSignAndEncryptDnsNamesAsync () + { + if (!IsEnabled) + return Task.CompletedTask; + + return base.TestCanSignAndEncryptDnsNamesAsync (); + } + [Test] public override void TestSecureMimeEncapsulatedSigning () { diff --git a/UnitTests/Cryptography/SqliteCertificateDatabaseTests.cs b/UnitTests/Cryptography/SqliteCertificateDatabaseTests.cs index 68de7e0464..7a17edd5d0 100644 --- a/UnitTests/Cryptography/SqliteCertificateDatabaseTests.cs +++ b/UnitTests/Cryptography/SqliteCertificateDatabaseTests.cs @@ -43,7 +43,7 @@ public class SqliteCertificateDatabaseTests : IDisposable public SqliteCertificateDatabaseTests () { - var rsa = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = SecureMimeTestsBase.RsaCertificate; dataDir = Path.Combine (TestHelper.ProjectDir, "TestData", "smime"); if (File.Exists ("sqlite.db")) @@ -71,7 +71,7 @@ public void Dispose () } [Test] - public void TestAutoUpgrade () + public void TestAutoUpgradeVersion0 () { var path = Path.Combine (dataDir, "smimev0.db"); const string tmp = "smimev0-tmp.db"; @@ -97,6 +97,33 @@ public void TestAutoUpgrade () } } + [Test] + public void TestAutoUpgradeVersion1 () + { + var path = Path.Combine (dataDir, "smimev1.db"); + const string tmp = "smimev1-tmp.db"; + + if (File.Exists (tmp)) + File.Delete (tmp); + + File.Copy (path, tmp); + + using (var dbase = new SqliteCertificateDatabase (tmp, "no.secret")) { + var root = chain[chain.Length - 1]; + + // Verify that we can select the Root Certificate + bool trustedAnchor = false; + foreach (var record in dbase.Find (null, true, X509CertificateRecordFields.Certificate)) { + if (record.Certificate.Equals (root)) { + trustedAnchor = true; + break; + } + } + + Assert.That (trustedAnchor, Is.True, "Did not find the MimeKit UnitTests trusted anchor"); + } + } + static void AssertFindBy (Org.BouncyCastle.Utilities.Collections.ISelector selector, X509Certificate expected) { using (var dbase = new SqliteCertificateDatabase ("sqlite.db", "no.secret")) { diff --git a/UnitTests/Cryptography/TemporarySecureMimeContextTests.cs b/UnitTests/Cryptography/TemporarySecureMimeContextTests.cs index a437fa1ff4..0dd2a62925 100644 --- a/UnitTests/Cryptography/TemporarySecureMimeContextTests.cs +++ b/UnitTests/Cryptography/TemporarySecureMimeContextTests.cs @@ -36,7 +36,7 @@ public class TemporarySecureMimeContextTests [Test] public void TestImportX509Certificate2 () { - var rsa = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = SecureMimeTestsBase.RsaCertificate; var certificate = new X509Certificate2 (rsa.FileName, "no.secret", X509KeyStorageFlags.Exportable); using (var ctx = new TemporarySecureMimeContext ()) { @@ -56,7 +56,7 @@ public void TestImportX509Certificate2 () [Test] public async Task TestImportX509Certificate2Async () { - var rsa = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = SecureMimeTestsBase.RsaCertificate; var certificate = new X509Certificate2 (rsa.FileName, "no.secret", X509KeyStorageFlags.Exportable); using (var ctx = new TemporarySecureMimeContext ()) { diff --git a/UnitTests/Cryptography/X509CertificateChainTests.cs b/UnitTests/Cryptography/X509CertificateChainTests.cs index 7fec7d7500..2d9367a780 100644 --- a/UnitTests/Cryptography/X509CertificateChainTests.cs +++ b/UnitTests/Cryptography/X509CertificateChainTests.cs @@ -46,7 +46,7 @@ static string GetTestDataPath (string relative) [Test] public void TestArgumentExceptions () { - var rsa = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = SecureMimeTestsBase.RsaCertificate; var chain = new X509CertificateChain (); CmsSigner signer; @@ -103,7 +103,7 @@ public void TestAddRemoveRange () [Test] public void TestBasicFunctionality () { - var rsa = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = SecureMimeTestsBase.RsaCertificate; var certs = rsa.Chain; var chain = new X509CertificateChain (); diff --git a/UnitTests/Cryptography/X509CertificateGenerator.cs b/UnitTests/Cryptography/X509CertificateGenerator.cs index acef88b4e9..4e6a24e6b4 100644 --- a/UnitTests/Cryptography/X509CertificateGenerator.cs +++ b/UnitTests/Cryptography/X509CertificateGenerator.cs @@ -43,10 +43,13 @@ using Org.BouncyCastle.X509.Extension; using Org.BouncyCastle.Asn1.X9; +using MimeKit; + namespace UnitTests.Cryptography { class X509CertificateGenerator { static readonly Dictionary X509NameOidMapping; + static readonly Dictionary X509SubjectAlternativeTagMapping; static readonly char[] EqualSign = new char[] { '=' }; static X509CertificateGenerator () @@ -94,6 +97,11 @@ static X509CertificateGenerator () { "UnstructuredName", X509Name.UnstructuredName }, { "UniqueIdentifier", X509Name.UniqueIdentifier }, }; + + X509SubjectAlternativeTagMapping = new Dictionary (StringComparer.OrdinalIgnoreCase) { + { "Rfc822Name", GeneralName.Rfc822Name }, + { "DnsName", GeneralName.DnsName }, + }; } static AsymmetricCipherKeyPair LoadAsymmetricCipherKeyPair (string fileName) @@ -208,6 +216,8 @@ public CertificateOptions () internal IList Values { get; } + internal List SubjectAlternativeNames { get; private set; } + public void Add (DerObjectIdentifier oid, string value) { if (oid == X509Name.CN || oid == X509Name.E) @@ -224,6 +234,25 @@ public void Add (string property, string value) Add (oid, value); } + + public void AddSubjectAlternativeName (string property, string value) + { + if (!X509SubjectAlternativeTagMapping.TryGetValue (property, out int tagNo)) + throw new ArgumentException ($"Unknown property: {property}", nameof (property)); + + SubjectAlternativeNames ??= new List (); + switch (tagNo) { + case GeneralName.Rfc822Name: + SubjectAlternativeNames.Add (new GeneralName (tagNo, MailboxAddress.EncodeAddrspec (value))); + break; + case GeneralName.DnsName: + SubjectAlternativeNames.Add (new GeneralName (tagNo, MailboxAddress.IdnMapping.Encode (value))); + break; + default: + SubjectAlternativeNames.Add (new GeneralName (tagNo, value)); + break; + } + } } public sealed class GeneratorOptions @@ -375,6 +404,11 @@ public static X509Certificate[] Generate (GeneratorOptions options, PrivateKeyOp generator.AddExtension (X509Extensions.SubjectKeyIdentifier, false, new SubjectKeyIdentifierStructure (key.Public)); + if (certificateOptions.SubjectAlternativeNames != null) { + var altNames = new GeneralNames (certificateOptions.SubjectAlternativeNames.ToArray ()); + generator.AddExtension (X509Extensions.SubjectAlternativeName, false, altNames); + } + if (issuerCertificate != null) generator.AddExtension (X509Extensions.AuthorityKeyIdentifier, false, new AuthorityKeyIdentifierStructure (issuerCertificate)); @@ -513,6 +547,13 @@ public static X509Certificate[] Generate (string cfg) throw new FormatException ($"Unknown [Subject] property: {property}"); } break; + case "subjectalternativenames": + try { + certificate.AddSubjectAlternativeName (property, value); + } catch (ArgumentException) { + throw new FormatException ($"Unknown [SubjectAlternativeNames] property: {property}"); + } + break; case "generator": switch (property.ToLowerInvariant ()) { case "basicconstraints": diff --git a/UnitTests/Cryptography/X509CertificateRecordTests.cs b/UnitTests/Cryptography/X509CertificateRecordTests.cs index f56fadf690..bfbd752d73 100644 --- a/UnitTests/Cryptography/X509CertificateRecordTests.cs +++ b/UnitTests/Cryptography/X509CertificateRecordTests.cs @@ -37,7 +37,7 @@ public class X509CertificateRecordTests [Test] public void TestArgumentExceptions () { - var rsa = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = SecureMimeTestsBase.RsaCertificate; var signer = new CmsSigner (rsa.FileName, "no.secret"); AsymmetricCipherKeyPair keyPair; @@ -64,14 +64,21 @@ static void AssertCertificateProperties (X509CertificateRecord record, X509Certi Assert.That (record.IssuerName, Is.EqualTo (certificate.IssuerDN.ToString ()), "IssuerName"); Assert.That (record.SerialNumber, Is.EqualTo (certificate.SerialNumber.ToString ()), "SerialNumber"); Assert.That (record.SubjectName, Is.EqualTo (certificate.SubjectDN.ToString ()), "SubjectName"); - Assert.That (record.SubjectEmail, Is.EqualTo (certificate.GetSubjectEmailAddress ()), "SubjectEmail"); + Assert.That (record.SubjectEmail, Is.EqualTo (certificate.GetSubjectEmailAddress (true)), "SubjectEmail"); Assert.That (record.Fingerprint, Is.EqualTo (certificate.GetFingerprint ()), "Fingerprint"); + + var certDomains = certificate.GetSubjectDnsNames (true); + var recordDomains = record.SubjectDnsNames; + + Assert.That (record.SubjectDnsNames.Length, Is.EqualTo (certDomains.Length), "SubjectDnsNames.Length"); + for (int i = 0; i < recordDomains.Length; i++) + Assert.That (recordDomains[i], Is.EqualTo (certDomains[i]), $"SubjectDnsNames[{i}]"); } [Test] public void TestDefaultValues () { - var rsa = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = SecureMimeTestsBase.RsaCertificate; var signer = new CmsSigner (rsa.FileName, "no.secret"); AsymmetricCipherKeyPair keyPair; X509CertificateRecord record; diff --git a/UnitTests/Cryptography/X509CertificateStoreTests.cs b/UnitTests/Cryptography/X509CertificateStoreTests.cs index c9798bdff4..945fe0921e 100644 --- a/UnitTests/Cryptography/X509CertificateStoreTests.cs +++ b/UnitTests/Cryptography/X509CertificateStoreTests.cs @@ -44,7 +44,7 @@ static string GetTestDataPath (string relative) [Test] public void TestArgumentExceptions () { - var rsa = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = SecureMimeTestsBase.RsaCertificate; var store = new X509CertificateStore (); Assert.Throws (() => store.Add (null)); @@ -193,7 +193,7 @@ public void TestImportExportMultipleCertificates () [Test] public void TestImportExportPkcs12 () { - var rsa = SecureMimeTestsBase.SupportedCertificates.FirstOrDefault (c => c.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral); + var rsa = SecureMimeTestsBase.RsaCertificate; var store = new X509CertificateStore (); store.Import (rsa.FileName, "no.secret"); diff --git a/UnitTests/TestData/smime/dnsnames/smime.cfg b/UnitTests/TestData/smime/dnsnames/smime.cfg new file mode 100644 index 0000000000..f79c7121f1 --- /dev/null +++ b/UnitTests/TestData/smime/dnsnames/smime.cfg @@ -0,0 +1,24 @@ +[PrivateKey] +Algorithm = RSA +BitLength = 4096 +FileName = smime.key + +[Subject] +CountryName = US +StateOrProvinceName = Massachusetts +LocalityName = Boston +CommonName = MimeKit UnitTests + +[SubjectAlternativeNames] +DnsName = domain-wide.example.com +DnsName = Lothlórien.com + +[Generator] +BasicConstraints = critical, CA:false +DaysValid = 3650 +Issuer = ..\intermediate2.pfx +IssuerPassword = no.secret +KeyUsage = critical, digitalSignature, keyEncipherment, nonRepudiation +SignatureAlgorithm = SHA256WithRSA +#Output = smime.pfx +Password = no.secret diff --git a/UnitTests/TestData/smime/dnsnames/smime.key b/UnitTests/TestData/smime/dnsnames/smime.key new file mode 100644 index 0000000000..612a10438d --- /dev/null +++ b/UnitTests/TestData/smime/dnsnames/smime.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJJwIBAAKCAgEAwY5AwH5ZvvWkrCQygwzTlqnPCky4E1iavgmFR17rllzAirCg +DxEF+VAPTBcPqDiXB6omBifY6evknFmqBi+ia7khxkRhE4irhZb1zLeKJa9Qe2rL +polTeA52FJMGPIxT/MBLEfQlYucP18AMIoVUpcOsMkryj9xAClsVOBESCdiRLrV6 +rd7W4iTRd7gbNJZlRgUsAiBHAbXoQf5HE2gZ6t/3YxrJcsX3LcKIVhDJL6J+AIpr +zVNJ9ElG7TsYBwVxHcPc1kzdqUStQ4fQR40TKdwOWYCybfwbs6I9tZxOaY7HaUER +QrFHdaX61RYDT1N5PVYVjgPOk2eItKzL8aXyCE7uEy45Xin9eDWv/eb4MqUhFy2T +H0jlZmEMoV3mg74Qmr+94zsY3NZkQL5pBacb/HBA49MBKPJBM+rdwBybzpVMIZ0b +McvjnMYcSaFxJhzoV+4pPB1Is2BFEMB4R1etlHufbYM/1NVXF0vWCP09V77D7WsV +PXJDjNFVZdu9ETAcD1KfvklXDZDwqlQvO43DlgTEalcNBTaMlr9Pbsj55rJLC+yF +xX3efPCeEF71YYMY94l3Hkj32bpqa0UoPzIPo9U03rY+B5J8C/t0wm17E/kg3y2O +KM/ykLZteYhSiviMzXDWRQOSEOHTcngNcTFbVRE+fgnZAkH6CWKrr3ZVSa0CAwEA +AQKCAgBqlmW+G1ZcvHU0frJ6TIPwgg6Lw3fS34ZHhIKqrPDbWrSFK4LZCSzbAGWM +J17t6kvxYpeR6Duhhc/c8duZkH3HCKo6vskesrKR3HH7jE89NXACpusDCLi4cm5A +Ij7a9QQDOfmdJ2+3KTsmOpH0KKxWpIydHXy6EDYL/eCPgYcHeQVqTXIDcaWv30qi +vPXuXavjhVGY0iGIJZ6DSP3nB/rNxww6vTOWSsI1ptzhWFkSLE9rCM8YwPcG2Zt/ +ZH100GBcXdGtCaM/ZZxshcwCuwOEl7QnQaIAw0aWA5AsBKmBo6jYo4ZXzbxmY0Lg +OUEVXAh16IPyMtJ9hhRYOpgMuK+xQlRrlW3UaF5c+4b46vN87LXItPqXNbXEfk0M +SYiv0bJg1gCzqj9noSDEnxRQaSoYH79ucBgUELT9DkrTs5UN5DP7bU9OvNK4SG63 +XsFPV+MIDe4+0YaKem77vpY2mlVVB9YwQaPdX44rkPbX40Zci8j7A14cjP5+JTwf +r3B2vGYrNcKBxF+HH2lT1uZanaFC5VdXmFD9wsPRe+N6e/t5YaVeTPOjjUCOjQzZ +FQT+A8sIOrKdS9XLaSKuVLlBQsO2wJNVgh0wqwezY4PvdJqgudQ141uIuJ2yeLrt +oW5yRxv6wK290piZryg3dbSH37MqNMZOFh8vpcPv3xVg+RAsWQKCAQEA5LpZEHh2 +Ebp7t9IBaJCIj+YQsRsL7DKt2aXFkMilaXkVJQSW2zDiE/Lbt2mjeAXhcrztVLRF +RadDDoh7OaljBw+1rH9ok6wpatzrvuhGekJyWpKCpaRXhiO/oKYi8NCTkxuUCQv6 +nLG8/4/eRn6RGnXp7RGmPC7Q0tw+3V9/phTQCe8d2CLKE7tnLYN6pgGDDkt8Nq2/ +q3TeZ+hX2EFKhZTFBgnx97rlSLFsb9neQG7fLcTUBc9rlhVE7FgyT68vNpky3tEZ +2d94f/bKbFEHGcvKZcjhCaKIjusbBG27JSoqFkvV9TN6mxtskuAUy8SBYaMtkNS3 +JL39jkVJJgV9rwKCAQEA2KJQOjO/41qRvLNaGTjc+CvK+0Z2lQbca8FLhI8G0RRK +Zn3QzR9WTYqQT+zREkFaCZAtJORKDfGA0ZvVhIAXwnya6tCshNQ012cl1jc6ZMqH +A9UORwOt+OewZDOKPqFPaNNZ98+sLNiEMpQ3YbyFgmbZV0a1cxdAYwNL4dz+IfZI +WYu69xbt0qMzZvMhavyIscOlKpi93OkhBw5n0qU/6QWNjFAn5oXoc8rVF5FMS5vd +nN1KSZrsNoiYQbDz/IQcO+kEUGQZUMMP0hfCr9heIYHFfl9ZFNj4CP9v5DUGmpur +el9RE75GUkggsc6YrLp29jXmvF3QQAmhCeryNMgBYwKCAQAJI/VJNjcpsDUffHH3 +9sauUXhbS4RndQMDjp9dkNcjZuZUa2GH8uUl/O+Q3dTdiAahajFl0CpwhSWl4Ahk +noNJlfQhp5nLRPcGwTtejrO6UQt22SIFcpLY1nbi+aCt1PgAyfpZfjQOrP+ritlM +IeS0lP+7LJhjEU/hDVIp0JYuUeiabQbZS1KeBUAzTmzJU4gkOxoEqV7egDYfGubf +yoQq4G4bNqyHxN1C0WxO7/r0wjmC/7mlXcuj3Me7Vi70hkCxwt+Ijfyle0u6eWdP +etF4028MMEHl+6vPYk/bFnODIbM63t73BI6iwi7Nk8zg88Jj33yDrCyBeGI4nEY3 +EcMbAoIBAFoIUzltKnGtwWXgUDCtTkChyrFVnpDfEhqCcgU8gAPC4Azqey3UuURu +sv1Umatxl57j2a88ZX6YAQacMkfoCHfe2299nEV0ACYJi1MVDuK2vRgdotpmsBYD +DG8IcIsI9XzGYdy45YCZ149BxCaNeBsy7V71VxHm9u5vf0j2VHP+7CCzDtgEIoDp +LMK7hwb0v0bJ3cnvQvEdvok1Nnb4ELCiiypmYb7PpkUBZkBuNXwy4g6AdZBTn5om +eMjMZwpqSWWouQ9EGrVS7C9Piq0USkK4sUCNFfOxHJx4tKLuWrlEuyaXmJWQ/Z7S +tSvQhek7cZdv3V4pyxPiLJh3mYPQH6sCggEAU06es8fR8nXxz3+a1KUkqOSPrRfm +vhnPnf5cxP5WT5A8ReFpUkoWWAFQSdlJKBwgognUfHeYyTBdBQZvjgF27QQHwNQh +lihKso7ixtjfVzavUtvTnCnjkYp/W/MkpVl5VKJ1k0nXNyD4qeSxrVDB18SkZ1Mg ++UhQseyjraYCkLzW4R4zvxDeNuUjKW+nKFJao2H08pMI+2VvB4904tNLRTcheWty +dG0Pt+lGU+K2XHxVmcIPjoQAaiVSn7GGW9a2Illc6Dk0qFPj5EoY5qIaKyB+CrEo +3QAGaeJucXZULbO9VyfRQAgblsWsoWGso+sZChWuAc/7uEmDgLZ/L90HPQ== +-----END RSA PRIVATE KEY----- diff --git a/UnitTests/TestData/smime/dnsnames/smime.pfx b/UnitTests/TestData/smime/dnsnames/smime.pfx new file mode 100644 index 0000000000..00847f97b5 Binary files /dev/null and b/UnitTests/TestData/smime/dnsnames/smime.pfx differ diff --git a/UnitTests/TestData/smime/genkeys.sh b/UnitTests/TestData/smime/genkeys.sh index e1249fcff2..a5c2c2136f 100644 --- a/UnitTests/TestData/smime/genkeys.sh +++ b/UnitTests/TestData/smime/genkeys.sh @@ -28,3 +28,7 @@ fi if [ ! -e "ec/smime.key" ]; then openssl ecparam -name secp384r1 -genkey -noout -out ec/smime.key > /dev/null fi + +if [ ! -e "dnsnames/smime.key" ]; then + openssl genrsa -out dnsnames/smime.key 4096 > /dev/null +fi diff --git a/UnitTests/TestData/smime/smimev1.db b/UnitTests/TestData/smime/smimev1.db new file mode 100644 index 0000000000..6dfc78d972 Binary files /dev/null and b/UnitTests/TestData/smime/smimev1.db differ