diff --git a/MimeKit/Cryptography/BouncyCastleSecureMimeContext.cs b/MimeKit/Cryptography/BouncyCastleSecureMimeContext.cs index 6088bbfe54..d8885c26ae 100644 --- a/MimeKit/Cryptography/BouncyCastleSecureMimeContext.cs +++ b/MimeKit/Cryptography/BouncyCastleSecureMimeContext.cs @@ -56,6 +56,8 @@ using IssuerAndSerialNumber = Org.BouncyCastle.Asn1.Cms.IssuerAndSerialNumber; using MimeKit.IO; +using System.Linq; +using Org.BouncyCastle.Tls; namespace MimeKit.Cryptography { /// @@ -169,6 +171,9 @@ protected virtual HttpClient HttpClient { /// generally issued by a certificate authority (CA). /// This method is used to build a certificate chain while verifying /// signed content. + /// It is critical to always load the designated trust anchors + /// and not the anchor in the end certificate when building a certificate chain + /// to validated trust. /// /// The trusted anchors. protected abstract ISet GetTrustedAnchors (); @@ -325,6 +330,9 @@ CmsSignedDataStreamGenerator CreateSignedDataGenerator (CmsSigner signer) Stream Sign (CmsSigner signer, Stream content, bool encapsulate, CancellationToken cancellationToken) { + if (CheckCertificateRevocation) + ValidateCertificateChain (signer.CertificateChain, DateTime.UtcNow, cancellationToken); + var signedData = CreateSignedDataGenerator (signer); var memory = new MemoryBlockStream (); @@ -339,6 +347,9 @@ Stream Sign (CmsSigner signer, Stream content, bool encapsulate, CancellationTok async Task SignAsync (CmsSigner signer, Stream content, bool encapsulate, CancellationToken cancellationToken) { + if (CheckCertificateRevocation) + await ValidateCertificateChainAsync (signer.CertificateChain, DateTime.UtcNow, cancellationToken); + var signedData = CreateSignedDataGenerator (signer); var memory = new MemoryBlockStream (); @@ -694,20 +705,31 @@ X509Certificate GetCertificate (IStore store, SignerID signer) /// The certificate chain, including the specified certificate. protected IList BuildCertificateChain (X509Certificate certificate) { - var selector = new X509CertStoreSelector { - Certificate = certificate - }; + var selector = new X509CertStoreSelector (); - var intermediates = new X509CertificateStore (); - intermediates.Add (certificate); + var userCertificateStore = new X509CertificateStore (); + userCertificateStore.Add (certificate); - var parameters = new PkixBuilderParameters (GetTrustedAnchors (), selector) { + var issuerStore = GetTrustedAnchors (); + var anchorStore = new X509CertificateStore (); + + foreach (var anchor in issuerStore) { + anchorStore.Add (anchor.TrustedCert); + } + + var parameters = new PkixBuilderParameters (issuerStore, selector) { ValidityModel = PkixParameters.PkixValidityModel, IsRevocationEnabled = false, Date = DateTime.UtcNow }; - parameters.AddStoreCert (intermediates); - parameters.AddStoreCert (GetIntermediateCertificates ()); + parameters.AddStoreCert (userCertificateStore); + + var intermediateStore = GetIntermediateCertificates (); + + foreach (var intermediate in intermediateStore.EnumerateMatches (new X509CertStoreSelector ())) + anchorStore.Add (intermediate); + + parameters.AddStoreCert (anchorStore); var builder = new PkixCertPathBuilder (); var result = builder.Build (parameters); @@ -720,6 +742,158 @@ protected IList BuildCertificateChain (X509Certificate certific return chain; } + /// + /// Validate an S/MIME certificate chain. + /// + /// + /// Validates an S/MIME certificate chain. + /// Downloads the CRLs for each certificate in the chain and then validates that the chain + /// is both valid and that none of the certificates in the chain have been revoked or compromised + /// in any way. + /// + /// true if the certificate chain is valid; otherwise, false. + /// The S/MIME certificate chain. + /// The date and time to use for validation. + /// The cancellation token. + /// + /// is . + /// + /// + /// is empty or contains a certificate. + /// + public bool ValidateCertificateChain (X509CertificateChain chain, DateTime dateTime, CancellationToken cancellationToken = default) + { + if (chain == null) + throw new ArgumentNullException (nameof (chain)); + + if (chain.Count == 0) + throw new ArgumentException ("The certificate chain must contain at least one certificate.", nameof (chain)); + + if (chain.Any (certificate => certificate == null)) + throw new ArgumentException ("The certificate chain contains at least one null certificate.", nameof (chain)); + + var selector = new X509CertStoreSelector (); + + var userCertificateStore = new X509CertificateStore (); + userCertificateStore.AddRange (chain); + + var issuerStore = GetTrustedAnchors (); + var anchorStore = new X509CertificateStore (); + + foreach (var anchor in issuerStore) { + anchorStore.Add (anchor.TrustedCert); + } + + var parameters = new PkixBuilderParameters (issuerStore, selector) { + ValidityModel = PkixParameters.PkixValidityModel, + IsRevocationEnabled = false, + Date = DateTime.UtcNow + }; + parameters.AddStoreCert (userCertificateStore); + + if (CheckCertificateRevocation) { + foreach (var certificate in chain) + DownloadCrls (certificate, cancellationToken); + } + + var intermediateStore = GetIntermediateCertificates (); + + foreach (var intermediate in intermediateStore.EnumerateMatches (new X509CertStoreSelector ())) { + anchorStore.Add (intermediate); + if (CheckCertificateRevocation) + DownloadCrls (intermediate, cancellationToken); + } + + parameters.AddStoreCert (anchorStore); + + if (CheckCertificateRevocation) + parameters.AddStoreCrl (GetCertificateRevocationLists ()); + + try { + var builder = new PkixCertPathBuilder (); + builder.Build (parameters); + return true; + } catch { + return false; + } + } + + /// + /// Validate an S/MIME certificate chain. + /// + /// + /// Asynchronously validates an S/MIME certificate chain. + /// Downloads the CRLs for each certificate in the chain and then validates that the chain + /// is both valid and that none of the certificates in the chain have been revoked or compromised + /// in any way. + /// + /// true if the certificate chain is valid; otherwise, false. + /// The S/MIME certificate chain. + /// The date and time to use for validation. + /// The cancellation token. + /// + /// is . + /// + /// + /// is empty or contains a certificate. + /// + public async Task ValidateCertificateChainAsync (X509CertificateChain chain, DateTime dateTime, CancellationToken cancellationToken = default) + { + if (chain == null) + throw new ArgumentNullException (nameof (chain)); + + if (chain.Count == 0) + throw new ArgumentException ("The certificate chain must contain at least one certificate.", nameof (chain)); + + if (chain.Any (certificate => certificate == null)) + throw new ArgumentException ("The certificate chain contains at least one null certificate.", nameof (chain)); + + var selector = new X509CertStoreSelector (); + + var userCertificateStore = new X509CertificateStore (); + userCertificateStore.AddRange (chain); + + var issuerStore = GetTrustedAnchors (); + var anchorStore = new X509CertificateStore (); + + foreach (var anchor in issuerStore) { + anchorStore.Add (anchor.TrustedCert); + } + + var parameters = new PkixBuilderParameters (issuerStore, selector) { + ValidityModel = PkixParameters.PkixValidityModel, + IsRevocationEnabled = false, + Date = DateTime.UtcNow + }; + parameters.AddStoreCert (userCertificateStore); + + if (CheckCertificateRevocation) { + foreach (var certificate in chain) + await DownloadCrlsAsync (certificate, cancellationToken).ConfigureAwait (false); + } + + var intermediateStore = GetIntermediateCertificates (); + + foreach (var intermediate in intermediateStore.EnumerateMatches (new X509CertStoreSelector ())) { + anchorStore.Add (intermediate); + if (CheckCertificateRevocation) + await DownloadCrlsAsync (intermediate, cancellationToken).ConfigureAwait (false); + } + + parameters.AddStoreCert (anchorStore); + + if (CheckCertificateRevocation) + parameters.AddStoreCrl (GetCertificateRevocationLists ()); + + try { + var builder = new PkixCertPathBuilder (); + builder.Build (parameters); + return true; + } catch { + return false; + } + } + PkixCertPath BuildCertPath (ISet anchors, IStore certificates, IStore crls, X509Certificate certificate, DateTime signingTime) { var selector = new X509CertStoreSelector { @@ -995,7 +1169,7 @@ static IEnumerable EnumerateCrlDistributionPointUrls (X509Certificate ce } } - void DownloadCrls (X509Certificate certificate, CancellationToken cancellationToken) + void DownloadCrls (X509Certificate certificate, CancellationToken cancellationToken = default) { var nextUpdate = GetNextCertificateRevocationListUpdate (certificate.IssuerDN); var now = DateTime.UtcNow; @@ -1125,10 +1299,15 @@ DigitalSignatureCollection GetDigitalSignatures (CmsSignedDataParser parser, Can } var anchors = GetTrustedAnchors (); + var intermediates = GetIntermediateCertificates (); if (CheckCertificateRevocation) { foreach (var anchor in anchors) DownloadCrls (anchor.TrustedCert, cancellationToken); + + foreach (X509Certificate intermediate in intermediates.EnumerateMatches(new X509CertStoreSelector())) { + DownloadCrls (intermediate, cancellationToken); + } } try { @@ -1179,10 +1358,15 @@ async Task GetDigitalSignaturesAsync (CmsSignedDataP } var anchors = GetTrustedAnchors (); + var intermediates = GetIntermediateCertificates (); if (CheckCertificateRevocation) { foreach (var anchor in anchors) await DownloadCrlsAsync (anchor.TrustedCert, cancellationToken).ConfigureAwait (false); + + foreach (X509Certificate intermediate in intermediates.EnumerateMatches (new X509CertStoreSelector ())) { + await DownloadCrlsAsync (intermediate, cancellationToken); + } } try { diff --git a/MimeKit/Cryptography/DefaultSecureMimeContext.cs b/MimeKit/Cryptography/DefaultSecureMimeContext.cs index 8954f95ab4..19606e965b 100644 --- a/MimeKit/Cryptography/DefaultSecureMimeContext.cs +++ b/MimeKit/Cryptography/DefaultSecureMimeContext.cs @@ -432,20 +432,19 @@ protected override ISet GetTrustedAnchors () /// The intermediate certificates. protected override IStore GetIntermediateCertificates () { - //var intermediates = new X509CertificateStore (); - //var selector = new X509CertStoreSelector (); - //var keyUsage = new bool[9]; + var intermediates = new X509CertificateStore (); + var selector = new X509CertStoreSelector (); + var keyUsage = new bool[9]; - //keyUsage[(int) X509KeyUsageBits.KeyCertSign] = true; - //selector.KeyUsage = keyUsage; + keyUsage[(int) X509KeyUsageBits.KeyCertSign] = true; + selector.KeyUsage = keyUsage; - //foreach (var record in dbase.Find (selector, false, X509CertificateRecordFields.Certificate)) { - // if (!record.Certificate.IsSelfSigned ()) - // intermediates.Add (record.Certificate); - //} + foreach (var record in dbase.Find (selector, false, X509CertificateRecordFields.Certificate)) { + if (!record.Certificate.IsSelfSigned ()) + intermediates.Add (record.Certificate); + } - //return intermediates; - return dbase; + return intermediates; } /// diff --git a/UnitTests/Cryptography/ApplicationPkcs7MimeTests.cs b/UnitTests/Cryptography/ApplicationPkcs7MimeTests.cs index e695b859d4..87bac04e0e 100644 --- a/UnitTests/Cryptography/ApplicationPkcs7MimeTests.cs +++ b/UnitTests/Cryptography/ApplicationPkcs7MimeTests.cs @@ -35,6 +35,19 @@ using MimeKit.Cryptography; using BCX509Certificate = Org.BouncyCastle.X509.X509Certificate; +using Org.BouncyCastle.Cms; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.OpenSsl; + +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.X509; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Cms; +using Org.BouncyCastle.Crypto.Encodings; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Parameters; namespace UnitTests.Cryptography { [TestFixture] diff --git a/UnitTests/Cryptography/SecureMimeTests.cs b/UnitTests/Cryptography/SecureMimeTests.cs index 424e11526a..385949fdb8 100644 --- a/UnitTests/Cryptography/SecureMimeTests.cs +++ b/UnitTests/Cryptography/SecureMimeTests.cs @@ -25,6 +25,7 @@ // using System.Net; +using System.Reflection; using System.Text; using System.Security.Cryptography.X509Certificates; @@ -79,7 +80,7 @@ public abstract class SecureMimeTestsBase public const string ThunderbirdName = "fejj@gnome.org"; public static readonly string[] RelativeConfigFilePaths = { - "certificate-authority.cfg", "intermediate1.cfg", "intermediate2.cfg", "dnsnames/smime.cfg", "dsa/smime.cfg", "ec/smime.cfg", "revoked/smime.cfg", "rsa/smime.cfg" + "certificate-authority.cfg", "intermediate1.cfg", "intermediate2.cfg", "dnsnames/smime.cfg", "dsa/smime.cfg", "ec/smime.cfg", "nochain/smime.cfg", "revoked/smime.cfg", "revokednochain/smime.cfg", "rsa/smime.cfg" }; public static readonly string[] StartComCertificates = { @@ -90,6 +91,7 @@ public abstract class SecureMimeTestsBase public static readonly SMimeCertificate[] SupportedCertificates; public static readonly SMimeCertificate[] SMimeCertificates; public static readonly SMimeCertificate RevokedCertificate; + public static readonly SMimeCertificate RevokedNoChainCertificate; public static readonly SMimeCertificate DomainCertificate; public static readonly SMimeCertificate RsaCertificate; @@ -157,6 +159,8 @@ static SecureMimeTestsBase () if (smime.EmailAddress.Equals ("revoked@mimekit.net", StringComparison.OrdinalIgnoreCase)) { RevokedCertificate = smime; + } else if (smime.EmailAddress.Equals ("revokednochain@mimekit.net", StringComparison.OrdinalIgnoreCase)) { + RevokedNoChainCertificate = smime; } else if (smime.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral) { if (!string.IsNullOrEmpty (smime.EmailAddress)) RsaCertificate = smime; @@ -197,6 +201,8 @@ static SecureMimeTestsBase () if (smime.EmailAddress.Equals ("revoked@mimekit.net", StringComparison.OrdinalIgnoreCase)) { RevokedCertificate = smime; + } else if (smime.EmailAddress.Equals ("revokednochain@mimekit.net", StringComparison.OrdinalIgnoreCase)) { + RevokedNoChainCertificate = smime; } else if (smime.PublicKeyAlgorithm == PublicKeyAlgorithm.RsaGeneral) { if (!string.IsNullOrEmpty (smime.EmailAddress)) RsaCertificate = smime; @@ -235,7 +241,8 @@ static SecureMimeTestsBase () CurrentCrls = new X509Crl [] { X509CrlGenerator.Generate (RootCertificate, RootKey, yesterday, threeMonthsFromNow), X509CrlGenerator.Generate (IntermediateCertificate1, IntermediateKey1, yesterday, threeMonthsFromNow), - X509CrlGenerator.Generate (IntermediateCertificate2, IntermediateKey2, yesterday, threeMonthsFromNow, RevokedCertificate.Certificate) + X509CrlGenerator.Generate (IntermediateCertificate2, IntermediateKey2, yesterday, threeMonthsFromNow, RevokedCertificate.Certificate), + X509CrlGenerator.Generate (IntermediateCertificate2, IntermediateKey2, yesterday, threeMonthsFromNow, RevokedNoChainCertificate.Certificate) }; CrlRequestUris = new Uri [] { @@ -245,9 +252,9 @@ static SecureMimeTestsBase () }; } - protected static Mock CreateMockHttpMessageHandler () + protected static HttpResponseMessage[] RevokedCertificateResponses () { - var responses = new HttpResponseMessage[] { + return new HttpResponseMessage[] { new HttpResponseMessage (HttpStatusCode.OK) { Content = new ByteArrayContent (CurrentCrls[0].GetEncoded ()) }, @@ -258,9 +265,27 @@ protected static Mock CreateMockHttpMessageHandler () Content = new ByteArrayContent (CurrentCrls[2].GetEncoded ()) } }; + } - var mockHttpMessageHandler = new Mock (MockBehavior.Strict); + protected static HttpResponseMessage[] RevokedNoChainCertificateResponses () + { + return new HttpResponseMessage[] { + new HttpResponseMessage (HttpStatusCode.OK) { + Content = new ByteArrayContent (CurrentCrls[0].GetEncoded ()) + }, + new HttpResponseMessage (HttpStatusCode.OK) { + Content = new ByteArrayContent (CurrentCrls[1].GetEncoded ()) + }, + new HttpResponseMessage (HttpStatusCode.OK) { + Content = new ByteArrayContent (CurrentCrls[3].GetEncoded ()) + } + }; + } + protected static Mock CreateMockHttpMessageHandler (HttpResponseMessage[] responses) + { + var mockHttpMessageHandler = new Mock (MockBehavior.Strict); + for (int i = 0; i < CrlRequestUris.Length; i++) { var requestUri = CrlRequestUris[i]; var response = responses[i]; @@ -343,11 +368,11 @@ protected void ImportTestCertificates (SecureMimeContext ctx) Assert.DoesNotThrow (() => ctx.Import (mimekitCertificate.FileName, "no.secret")); } - if (windows is null) { - // Import the obsolete CRLs (we want the S/MIME context to download the current CRLs) - foreach (var crl in ObsoleteCrls) - ctx.Import (crl); - } + // if (windows is null) { + // // Import the obsolete CRLs (we want the S/MIME context to download the current CRLs) + // foreach (var crl in ObsoleteCrls) + // ctx.Import (crl); + // } } protected SecureMimeTestsBase () @@ -2863,13 +2888,36 @@ void AssertCrlsRequested (Mock mockHttpMessageHandler) } } - protected void VerifyRevokedCertificate (BouncyCastleSecureMimeContext ctx, Mock mockHttpMessageHandler) + void AssertCrlsNotRequested (Mock mockHttpMessageHandler) { - var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up signing..." }; - var certificate = RevokedCertificate; + try { + for (int i = 0; i < CrlRequestUris.Length; i++) { + var requestUri = CrlRequestUris[i]; + mockHttpMessageHandler.Protected ().Verify ( + "SendAsync", + Times.Exactly (0), + ItExpr.Is (m => m.Method == HttpMethod.Get && m.RequestUri == requestUri), + ItExpr.IsAny ()); + } + } catch (Exception ex) { + Assert.Fail (ex.Message); + } + } + + protected void VerifyRevokedCertificate (BouncyCastleSecureMimeContext ctx, Mock mockHttpMessageHandler, SMimeCertificate certificate, bool validateCreate) + { + var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up signing..." }; + var signer = new CmsSigner (certificate.FileName, "no.secret"); + ctx.CheckCertificateRevocation = validateCreate; var multipart = MultipartSigned.Create (ctx, signer, body); + ctx.CheckCertificateRevocation = true; + + if (validateCreate) + AssertCrlsRequested (mockHttpMessageHandler); + else + AssertCrlsNotRequested (mockHttpMessageHandler); Assert.That (multipart.Count, Is.EqualTo (2), "The multipart/signed has an unexpected number of children."); @@ -2932,13 +2980,20 @@ protected void VerifyRevokedCertificate (BouncyCastleSecureMimeContext ctx, Mock } } - protected async Task VerifyRevokedCertificateAsync (BouncyCastleSecureMimeContext ctx, Mock mockHttpMessageHandler) + protected async Task VerifyRevokedCertificateAsync (BouncyCastleSecureMimeContext ctx, Mock mockHttpMessageHandler, SMimeCertificate certificate, bool validateCreate) { var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up signing..." }; - var certificate = RevokedCertificate; - + var signer = new CmsSigner (certificate.FileName, "no.secret"); + ctx.CheckCertificateRevocation = validateCreate; var multipart = await MultipartSigned.CreateAsync (ctx, signer, body); + ctx.CheckCertificateRevocation = true; + + if (validateCreate) + AssertCrlsRequested (mockHttpMessageHandler); + else + AssertCrlsNotRequested (mockHttpMessageHandler); + Assert.That (multipart.Count, Is.EqualTo (2), "The multipart/signed has an unexpected number of children."); @@ -3000,6 +3055,121 @@ protected async Task VerifyRevokedCertificateAsync (BouncyCastleSecureMimeContex Assert.That (innerException.Message, Does.EndWith (", reason: keyCompromise")); } } + + protected void VerifyCrlsResolvedWithBuildCertificateChain (BouncyCastleSecureMimeContext ctx, + Mock mockHttpMessageHandler) + { + var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up signing..." }; + var certificate = SupportedCertificates.Single(c => c.EmailAddress == "nochain@mimekit.net"); + + var signer = new CmsSigner (certificate.FileName, "no.secret"); + var multipart = MultipartSigned.Create (ctx, signer, body); + + Assert.That (multipart.Count, Is.EqualTo (2), "The multipart/signed has an unexpected number of children."); + + var protocol = multipart.ContentType.Parameters["protocol"]; + Assert.That (protocol, Is.EqualTo (ctx.SignatureProtocol), "The multipart/signed protocol does not match."); + + Assert.That (multipart[0], Is.InstanceOf (), "The first child is not a text part."); + Assert.That (multipart[1], Is.InstanceOf (), "The second child is not a detached signature."); + + var signatures = multipart.Verify (ctx); + Assert.That (signatures.Count, Is.EqualTo (1), "Verify returned an unexpected number of signatures."); + + AssertCrlsRequested (mockHttpMessageHandler); + + AssertValidSignatures (ctx, signatures); + } + + protected void VerifyCrlsResolved (BouncyCastleSecureMimeContext ctx, + Mock mockHttpMessageHandler) + { + var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up signing..." }; + var certificate = SupportedCertificates.Single (c => c.EmailAddress == "nochain@mimekit.net"); + + var signer = new CmsSigner (certificate.FileName, "no.secret"); + var multipart = MultipartSigned.Create (ctx, signer, body); + + Assert.That (multipart.Count, Is.EqualTo (2), "The multipart/signed has an unexpected number of children."); + + var protocol = multipart.ContentType.Parameters["protocol"]; + Assert.That (protocol, Is.EqualTo (ctx.SignatureProtocol), "The multipart/signed protocol does not match."); + + Assert.That (multipart[0], Is.InstanceOf (), "The first child is not a text part."); + Assert.That (multipart[1], Is.InstanceOf (), "The second child is not a detached signature."); + + var signatures = multipart.Verify (ctx); + Assert.That (signatures.Count, Is.EqualTo (1), "Verify returned an unexpected number of signatures."); + + AssertCrlsRequested (mockHttpMessageHandler); + + AssertValidSignatures (ctx, signatures); + } + + protected void VerifyCrlMissing (BouncyCastleSecureMimeContext ctx, string errorContent) + { + var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up signing..." }; + var certificate = SupportedCertificates.Single (c => c.EmailAddress == "nochain@mimekit.net"); + + var signer = new CmsSigner (certificate.FileName, "no.secret"); + using var multipart = MultipartSigned.Create (ctx, signer, body); + + Assert.That (multipart.Count, Is.EqualTo (2), "The multipart/signed has an unexpected number of children."); + + var protocol = multipart.ContentType.Parameters["protocol"]; + Assert.That (protocol, Is.EqualTo (ctx.SignatureProtocol), "The multipart/signed protocol does not match."); + + Assert.That (multipart[0], Is.InstanceOf (), "The first child is not a text part."); + Assert.That (multipart[1], Is.InstanceOf (), "The second child is not a detached signature."); + + var signatures = multipart.Verify (ctx); + Assert.That (signatures.Count, Is.EqualTo (1), "Verify returned an unexpected number of signatures."); + + var ex = Assert.Throws (() => signatures.Single ().Verify ()); + Assert.That (ex.Message, Does.StartWith ("Failed to verify digital signature chain: Certification path could not be validated.")); + Assert.That (ex.InnerException?.Message, Does.StartWith ("Certification path could not be validated.")); + Assert.That(ex.InnerException?.InnerException?.Message, Is.EquivalentTo ($"No CRLs found for issuer \"{errorContent}\"")); + } + + + protected void VerifyMimeEncapsulatedSigningWithContext (BouncyCastleSecureMimeContext ctx, + Mock mockHttpMessageHandler) + { + var cleartext = new TextPart ("plain") { Text = "This is some text that we'll end up signing..." }; + + + var certificate = SupportedCertificates.Single (c => c.EmailAddress == "nochain@mimekit.net"); + + var self = new MailboxAddress ("MimeKit UnitTests", certificate.EmailAddress); + var signed = ApplicationPkcs7Mime.Sign (ctx, self, DigestAlgorithm.Sha1, cleartext); + + AssertCrlsRequested (mockHttpMessageHandler); + + MimeEntity extracted; + + Assert.That (signed.SecureMimeType, Is.EqualTo (SecureMimeType.SignedData), "S/MIME type did not match."); + + var signatures = signed.Verify (ctx, out extracted); + + Assert.That (extracted, Is.InstanceOf (), "Extracted part is not the expected type."); + Assert.That (((TextPart) extracted).Text, Is.EqualTo (cleartext.Text), "Extracted content is not the same as the original."); + + Assert.That (signatures.Count, Is.EqualTo (1), "Verify returned an unexpected number of signatures."); + AssertValidSignatures (ctx, signatures); + + using (var signedData = signed.Content.Open ()) { + using (var stream = ctx.Verify (signedData, out signatures)) + extracted = MimeEntity.Load (stream); + + Assert.That (extracted, Is.InstanceOf (), "Extracted part is not the expected type."); + Assert.That (((TextPart) extracted).Text, Is.EqualTo (cleartext.Text), "Extracted content is not the same as the original."); + + Assert.That (signatures.Count, Is.EqualTo (1), "Verify returned an unexpected number of signatures."); + AssertValidSignatures (ctx, signatures); + } + + AssertCrlsRequested (mockHttpMessageHandler); + } } [TestFixture] @@ -3010,11 +3180,11 @@ class MyTemporarySecureMimeContext : TemporarySecureMimeContext public readonly Mock MockHttpMessageHandler; readonly HttpClient client; - public MyTemporarySecureMimeContext () : base (new SecureRandom (new CryptoApiRandomGenerator ())) + public MyTemporarySecureMimeContext (Mock? mockHttpMessageHandler = null) : base (new SecureRandom (new CryptoApiRandomGenerator ())) { CheckCertificateRevocation = false; - MockHttpMessageHandler = CreateMockHttpMessageHandler (); + MockHttpMessageHandler = mockHttpMessageHandler ?? CreateMockHttpMessageHandler (RevokedCertificateResponses ()); client = new HttpClient (MockHttpMessageHandler.Object); } @@ -3039,20 +3209,142 @@ protected override SecureMimeContext CreateContext () [Test] public void TestVerifyRevokedCertificate () { - using (var ctx = new MyTemporarySecureMimeContext () { CheckCertificateRevocation = true }) { + using (var ctx = new MyTemporarySecureMimeContext ()) { + ImportTestCertificates (ctx); + + VerifyRevokedCertificate (ctx, ctx.MockHttpMessageHandler, RevokedCertificate, false); + } + + using (var ctx = new MyTemporarySecureMimeContext ()) { + ImportTestCertificates (ctx); + + VerifyRevokedCertificate (ctx, ctx.MockHttpMessageHandler, RevokedCertificate, true); + } + + using (var ctx = new MyTemporarySecureMimeContext (CreateMockHttpMessageHandler (RevokedNoChainCertificateResponses ()))) { + ImportTestCertificates (ctx); + + VerifyRevokedCertificate (ctx, ctx.MockHttpMessageHandler, RevokedNoChainCertificate, false); + } + + using (var ctx = new MyTemporarySecureMimeContext (CreateMockHttpMessageHandler (RevokedNoChainCertificateResponses ()))) { ImportTestCertificates (ctx); - VerifyRevokedCertificate (ctx, ctx.MockHttpMessageHandler); + VerifyRevokedCertificate (ctx, ctx.MockHttpMessageHandler, RevokedNoChainCertificate, true); } } [Test] public async Task TestVerifyRevokedCertificateAsync () + { + using (var ctx = new MyTemporarySecureMimeContext ()) { + ImportTestCertificates (ctx); + + await VerifyRevokedCertificateAsync (ctx, ctx.MockHttpMessageHandler, RevokedCertificate, false); + } + + using (var ctx = new MyTemporarySecureMimeContext ()) { + ImportTestCertificates (ctx); + + await VerifyRevokedCertificateAsync (ctx, ctx.MockHttpMessageHandler, RevokedCertificate, true); + } + + using (var ctx = new MyTemporarySecureMimeContext (CreateMockHttpMessageHandler (RevokedNoChainCertificateResponses ()))) { + ImportTestCertificates (ctx); + + await VerifyRevokedCertificateAsync (ctx, ctx.MockHttpMessageHandler, RevokedNoChainCertificate, false); + } + + using (var ctx = new MyTemporarySecureMimeContext (CreateMockHttpMessageHandler (RevokedNoChainCertificateResponses ()))) { + ImportTestCertificates (ctx); + + await VerifyRevokedCertificateAsync (ctx, ctx.MockHttpMessageHandler, RevokedNoChainCertificate, true); + } + } + + [Test] + public void TestVerifyCrlsResolved () + { + using (var ctx = new MyTemporarySecureMimeContext () { CheckCertificateRevocation = true }) { + ImportTestCertificates (ctx); + + VerifyCrlsResolved (ctx, ctx.MockHttpMessageHandler); + } + } + + [Test] + public void TestMissingRootCrl () + { + var responses = new HttpResponseMessage[] + { + new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(CurrentCrls[1].GetEncoded()) }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(CurrentCrls[2].GetEncoded()) } + }; + var crlUrlIndexes = new[] { 1, 2 }; + var errorContent = RootCertificate.SubjectDN.ToString (); + + VerifyCrlMissingTest (responses, crlUrlIndexes, errorContent); + } + + [Test] + public void TestMissingPrimaryIntermediateCrl () + { + var responses = new HttpResponseMessage[] + { + new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(CurrentCrls[0].GetEncoded()) }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(CurrentCrls[2].GetEncoded()) } + }; + var crlUrlIndexes = new[] { 0, 2 }; + var errorContent = IntermediateCertificate1.SubjectDN.ToString (); + + VerifyCrlMissingTest (responses, crlUrlIndexes, errorContent); + } + + [Test] + public void TestMissingSecondaryIntermediateCrl () + { + var responses = new HttpResponseMessage[] + { + new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(CurrentCrls[0].GetEncoded()) }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(CurrentCrls[1].GetEncoded()) } + }; + var crlUrlIndexes = new[] { 0, 1 }; + var errorContent = IntermediateCertificate2.SubjectDN.ToString (); + + VerifyCrlMissingTest (responses, crlUrlIndexes, errorContent); + } + + private void VerifyCrlMissingTest (HttpResponseMessage[] responses, int[] crlUrlIndexes, string errorContent) + { + var mockHttpMessageHandler = new Mock (MockBehavior.Strict); + + for (int i = 0; i < crlUrlIndexes.Length; i++) { + var requestUri = CrlRequestUris[crlUrlIndexes[i]]; + var response = responses[i]; + + mockHttpMessageHandler + .Protected () + .Setup> ( + "SendAsync", + ItExpr.Is (m => m.Method == HttpMethod.Get && m.RequestUri == requestUri), + ItExpr.IsAny ()) + .ReturnsAsync (response); + } + + using (var ctx = new MyTemporarySecureMimeContext (mockHttpMessageHandler) { CheckCertificateRevocation = true }) { + ImportTestCertificates (ctx); + + VerifyCrlMissing (ctx, errorContent); + } + } + + [Test] + public virtual void TestVerifyCrlsResolvedWithSecureMimeEncapsulatedSigningWithContext () { using (var ctx = new MyTemporarySecureMimeContext () { CheckCertificateRevocation = true }) { ImportTestCertificates (ctx); - await VerifyRevokedCertificateAsync (ctx, ctx.MockHttpMessageHandler); + VerifyMimeEncapsulatedSigningWithContext (ctx, ctx.MockHttpMessageHandler); } } } @@ -3069,11 +3361,11 @@ public MySecureMimeContext () : this ("smime.db", "no.secret") { } - public MySecureMimeContext (string database, string password) : base (database, password) + public MySecureMimeContext (string database, string password, Mock? mockHttpMessageHandler = null) : base (database, password) { CheckCertificateRevocation = false; - MockHttpMessageHandler = CreateMockHttpMessageHandler (); + MockHttpMessageHandler = mockHttpMessageHandler?? CreateMockHttpMessageHandler (RevokedCertificateResponses ()); client = new HttpClient (MockHttpMessageHandler.Object); } @@ -3102,22 +3394,70 @@ static DefaultSecureMimeTests () [Test] public void TestVerifyRevokedCertificate () { - using (var ctx = new MySecureMimeContext ("revoked.db", "no.secret") { CheckCertificateRevocation = true }) { + using (var ctx = new MySecureMimeContext ("revoked.db", "no.secret")) { ImportTestCertificates (ctx); - VerifyRevokedCertificate (ctx, ctx.MockHttpMessageHandler); + VerifyRevokedCertificate (ctx, ctx.MockHttpMessageHandler, RevokedCertificate, false); } File.Delete ("revoked.db"); + + using (var ctx = new MySecureMimeContext ("revoked.db", "no.secret")) { + ImportTestCertificates (ctx); + + VerifyRevokedCertificate (ctx, ctx.MockHttpMessageHandler, RevokedCertificate, true); + } + + File.Delete ("revoked.db"); + + using (var ctx = new MySecureMimeContext ("revoked.db", "no.secret", CreateMockHttpMessageHandler (RevokedNoChainCertificateResponses ()))) { + ImportTestCertificates (ctx); + + VerifyRevokedCertificate (ctx, ctx.MockHttpMessageHandler, RevokedNoChainCertificate, false); + } + + File.Delete ("revoked.db"); + + using (var ctx = new MySecureMimeContext ("revoked.db", "no.secret", CreateMockHttpMessageHandler (RevokedNoChainCertificateResponses ()))) { + ImportTestCertificates (ctx); + + VerifyRevokedCertificate (ctx, ctx.MockHttpMessageHandler, RevokedNoChainCertificate, true); + } + + File.Delete ("revoked.db"); } [Test] public async Task TestVerifyRevokedCertificateAsync () { - using (var ctx = new MySecureMimeContext ("revoked-async.db", "no.secret") { CheckCertificateRevocation = true }) { + using (var ctx = new MySecureMimeContext ("revoked-async.db", "no.secret")) { + ImportTestCertificates (ctx); + + await VerifyRevokedCertificateAsync (ctx, ctx.MockHttpMessageHandler, RevokedCertificate, false); + } + + File.Delete ("revoked-async.db"); + + using (var ctx = new MySecureMimeContext ("revoked-async.db", "no.secret")) { + ImportTestCertificates (ctx); + + await VerifyRevokedCertificateAsync (ctx, ctx.MockHttpMessageHandler, RevokedCertificate, true); + } + + File.Delete ("revoked-async.db"); + + using (var ctx = new MySecureMimeContext ("revoked-async.db", "no.secret", CreateMockHttpMessageHandler (RevokedNoChainCertificateResponses ()))) { + ImportTestCertificates (ctx); + + await VerifyRevokedCertificateAsync (ctx, ctx.MockHttpMessageHandler, RevokedNoChainCertificate, false); + } + + File.Delete ("revoked-async.db"); + + using (var ctx = new MySecureMimeContext ("revoked-async.db", "no.secret", CreateMockHttpMessageHandler (RevokedNoChainCertificateResponses ()))) { ImportTestCertificates (ctx); - await VerifyRevokedCertificateAsync (ctx, ctx.MockHttpMessageHandler); + await VerifyRevokedCertificateAsync (ctx, ctx.MockHttpMessageHandler, RevokedNoChainCertificate, true); } File.Delete ("revoked-async.db"); diff --git a/UnitTests/Cryptography/X509CertificateGenerator.cs b/UnitTests/Cryptography/X509CertificateGenerator.cs index f1b007a09a..44d49ee4a0 100644 --- a/UnitTests/Cryptography/X509CertificateGenerator.cs +++ b/UnitTests/Cryptography/X509CertificateGenerator.cs @@ -326,6 +326,8 @@ public GeneratorOptions () public string Password { get; set; } public string SignatureAlgorithm { get; set; } + + public bool IncludeChain { get; set; } = true; } public static X509Certificate[] Generate (GeneratorOptions options, PrivateKeyOptions privateKeyOptions, CertificateOptions certificateOptions, out AsymmetricKeyParameter privateKey) @@ -518,14 +520,17 @@ public static X509Certificate[] Generate (GeneratorOptions options, PrivateKeyOp var certificate = generator.Generate (signatureFactory); var keyEntry = new AsymmetricKeyEntry (privateKey); - var chainEntries = new X509CertificateEntry[chain.Length + 1]; + var chainLength = options.IncludeChain ? chain.Length + 1 : 1; + var chainEntries = new X509CertificateEntry[chainLength]; var certificates = new X509Certificate[chain.Length + 1]; chainEntries[0] = new X509CertificateEntry (certificate); certificates[0] = certificate; + for (int i = 0; i < chain.Length; i++) { - chainEntries[i + 1] = new X509CertificateEntry (chain[i]); + if(options.IncludeChain) + chainEntries[i + 1] = new X509CertificateEntry (chain[i]); certificates[i + 1] = chain[i]; } @@ -667,6 +672,10 @@ public static X509Certificate[] Generate (string cfg, out AsymmetricKeyParameter case "signaturealgorithm": options.SignatureAlgorithm = value; break; + case "includechain": + options.IncludeChain = bool.Parse (value); + break; + default: throw new FormatException ($"Unknown [Generator] property: {property}"); } diff --git a/UnitTests/TestData/smime/genkeys.sh b/UnitTests/TestData/smime/genkeys.sh index a5c2c2136f..9ef60e4b07 100644 --- a/UnitTests/TestData/smime/genkeys.sh +++ b/UnitTests/TestData/smime/genkeys.sh @@ -32,3 +32,7 @@ fi if [ ! -e "dnsnames/smime.key" ]; then openssl genrsa -out dnsnames/smime.key 4096 > /dev/null fi + +if [ ! -e "nochain/smime.key" ]; then + openssl genrsa -out nochain/smime.key 4096 > /dev/null +fi diff --git a/UnitTests/TestData/smime/nochain/smime.cfg b/UnitTests/TestData/smime/nochain/smime.cfg new file mode 100644 index 0000000000..14554033a7 --- /dev/null +++ b/UnitTests/TestData/smime/nochain/smime.cfg @@ -0,0 +1,32 @@ +[PrivateKey] +Algorithm = RSA +BitLength = 4096 +FileName = smime.key + +[Subject] +CountryName = US +StateOrProvinceName = Massachusetts +LocalityName = Boston +CommonName = MimeKit UnitTests +EmailAddress = nochain@mimekit.net + +[Revocation] +DistributionPoint = https://mimekit.net/crls/intermediate2.crl + +[SMimeCapabilities] +capability.0 = AES256 +capability.1 = AES192 +capability.2 = AES128 +capability.3 = IDEA +capability.4 = 3DES + +[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 +IncludeChain = false \ No newline at end of file diff --git a/UnitTests/TestData/smime/nochain/smime.key b/UnitTests/TestData/smime/nochain/smime.key new file mode 100644 index 0000000000..612a10438d --- /dev/null +++ b/UnitTests/TestData/smime/nochain/smime.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/UnitTests/TestData/smime/nochain/smime.pfx b/UnitTests/TestData/smime/nochain/smime.pfx new file mode 100644 index 0000000000..1b1a4ee3e7 Binary files /dev/null and b/UnitTests/TestData/smime/nochain/smime.pfx differ diff --git a/UnitTests/TestData/smime/revokednochain/smime.cfg b/UnitTests/TestData/smime/revokednochain/smime.cfg new file mode 100644 index 0000000000..fffb031f66 --- /dev/null +++ b/UnitTests/TestData/smime/revokednochain/smime.cfg @@ -0,0 +1,31 @@ +[PrivateKey] +Algorithm = RSA +BitLength = 4096 + +[Subject] +CountryName = US +StateOrProvinceName = Massachusetts +LocalityName = Boston +CommonName = MimeKit UnitTests +EmailAddress = revokednochain@mimekit.net + +[Revocation] +DistributionPoint = https://mimekit.net/crls/intermediate2.crl + +[SMimeCapabilities] +capability.0 = AES256 +capability.1 = AES192 +capability.2 = AES128 +capability.3 = IDEA +capability.4 = 3DES + +[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 +IncludeChain = false diff --git a/UnitTests/TestData/smime/revokednochain/smime.pfx b/UnitTests/TestData/smime/revokednochain/smime.pfx new file mode 100644 index 0000000000..9ab47a6e8e Binary files /dev/null and b/UnitTests/TestData/smime/revokednochain/smime.pfx differ