Skip to content

Commit ea87b4d

Browse files
authored
Include Intermediate Certificate Resolution for S/MIME and Ensure Trust Validation during BuildCertificateChain() (#1124)
* Adding CRL checks during BuildCertificateChain Include intermediate certificate resolution. Adding tests to ensure CRLs are resolved outside of a revocation test. A new nochain certificate was created where the cert does not contain the whole certificate chain. This way the resolution can be exercised. * ValidateCertificateChain Move the IO operations to ValidateCertificateChain and ValidateCertificateChainAsync. Removed IO from BuildCertificateChain. * Fixup CRL tests
1 parent 49bb9d4 commit ea87b4d

11 files changed

+712
-49
lines changed

MimeKit/Cryptography/BouncyCastleSecureMimeContext.cs

+193-9
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
using IssuerAndSerialNumber = Org.BouncyCastle.Asn1.Cms.IssuerAndSerialNumber;
5757

5858
using MimeKit.IO;
59+
using System.Linq;
60+
using Org.BouncyCastle.Tls;
5961

6062
namespace MimeKit.Cryptography {
6163
/// <summary>
@@ -169,6 +171,9 @@ protected virtual HttpClient HttpClient {
169171
/// generally issued by a certificate authority (CA).</para>
170172
/// <para>This method is used to build a certificate chain while verifying
171173
/// signed content.</para>
174+
/// <para>It is critical to always load the designated trust anchors
175+
/// and not the anchor in the end certificate when building a certificate chain
176+
/// to validated trust.</para>
172177
/// </remarks>
173178
/// <returns>The trusted anchors.</returns>
174179
protected abstract ISet<TrustAnchor> GetTrustedAnchors ();
@@ -325,6 +330,9 @@ CmsSignedDataStreamGenerator CreateSignedDataGenerator (CmsSigner signer)
325330

326331
Stream Sign (CmsSigner signer, Stream content, bool encapsulate, CancellationToken cancellationToken)
327332
{
333+
if (CheckCertificateRevocation)
334+
ValidateCertificateChain (signer.CertificateChain, DateTime.UtcNow, cancellationToken);
335+
328336
var signedData = CreateSignedDataGenerator (signer);
329337
var memory = new MemoryBlockStream ();
330338

@@ -339,6 +347,9 @@ Stream Sign (CmsSigner signer, Stream content, bool encapsulate, CancellationTok
339347

340348
async Task<Stream> SignAsync (CmsSigner signer, Stream content, bool encapsulate, CancellationToken cancellationToken)
341349
{
350+
if (CheckCertificateRevocation)
351+
await ValidateCertificateChainAsync (signer.CertificateChain, DateTime.UtcNow, cancellationToken);
352+
342353
var signedData = CreateSignedDataGenerator (signer);
343354
var memory = new MemoryBlockStream ();
344355

@@ -694,20 +705,31 @@ X509Certificate GetCertificate (IStore<X509Certificate> store, SignerID signer)
694705
/// <returns>The certificate chain, including the specified certificate.</returns>
695706
protected IList<X509Certificate> BuildCertificateChain (X509Certificate certificate)
696707
{
697-
var selector = new X509CertStoreSelector {
698-
Certificate = certificate
699-
};
708+
var selector = new X509CertStoreSelector ();
700709

701-
var intermediates = new X509CertificateStore ();
702-
intermediates.Add (certificate);
710+
var userCertificateStore = new X509CertificateStore ();
711+
userCertificateStore.Add (certificate);
703712

704-
var parameters = new PkixBuilderParameters (GetTrustedAnchors (), selector) {
713+
var issuerStore = GetTrustedAnchors ();
714+
var anchorStore = new X509CertificateStore ();
715+
716+
foreach (var anchor in issuerStore) {
717+
anchorStore.Add (anchor.TrustedCert);
718+
}
719+
720+
var parameters = new PkixBuilderParameters (issuerStore, selector) {
705721
ValidityModel = PkixParameters.PkixValidityModel,
706722
IsRevocationEnabled = false,
707723
Date = DateTime.UtcNow
708724
};
709-
parameters.AddStoreCert (intermediates);
710-
parameters.AddStoreCert (GetIntermediateCertificates ());
725+
parameters.AddStoreCert (userCertificateStore);
726+
727+
var intermediateStore = GetIntermediateCertificates ();
728+
729+
foreach (var intermediate in intermediateStore.EnumerateMatches (new X509CertStoreSelector ()))
730+
anchorStore.Add (intermediate);
731+
732+
parameters.AddStoreCert (anchorStore);
711733

712734
var builder = new PkixCertPathBuilder ();
713735
var result = builder.Build (parameters);
@@ -720,6 +742,158 @@ protected IList<X509Certificate> BuildCertificateChain (X509Certificate certific
720742
return chain;
721743
}
722744

745+
/// <summary>
746+
/// Validate an S/MIME certificate chain.
747+
/// </summary>
748+
/// <remarks>
749+
/// <para>Validates an S/MIME certificate chain.</para>
750+
/// <para>Downloads the CRLs for each certificate in the chain and then validates that the chain
751+
/// is both valid and that none of the certificates in the chain have been revoked or compromised
752+
/// in any way.</para>
753+
/// </remarks>
754+
/// <returns><c>true</c> if the certificate chain is valid; otherwise, <c>false</c>.</returns>
755+
/// <param name="chain">The S/MIME certificate chain.</param>
756+
/// <param name="dateTime">The date and time to use for validation.</param>
757+
/// <param name="cancellationToken">The cancellation token.</param>
758+
/// <exception cref="ArgumentNullException">
759+
/// <paramref name="chain"/> is <see langword="null"/>.
760+
/// </exception>
761+
/// <exception cref="ArgumentException">
762+
/// <paramref name="chain"/> is empty or contains a <see langword="null"/> certificate.
763+
/// </exception>
764+
public bool ValidateCertificateChain (X509CertificateChain chain, DateTime dateTime, CancellationToken cancellationToken = default)
765+
{
766+
if (chain == null)
767+
throw new ArgumentNullException (nameof (chain));
768+
769+
if (chain.Count == 0)
770+
throw new ArgumentException ("The certificate chain must contain at least one certificate.", nameof (chain));
771+
772+
if (chain.Any (certificate => certificate == null))
773+
throw new ArgumentException ("The certificate chain contains at least one null certificate.", nameof (chain));
774+
775+
var selector = new X509CertStoreSelector ();
776+
777+
var userCertificateStore = new X509CertificateStore ();
778+
userCertificateStore.AddRange (chain);
779+
780+
var issuerStore = GetTrustedAnchors ();
781+
var anchorStore = new X509CertificateStore ();
782+
783+
foreach (var anchor in issuerStore) {
784+
anchorStore.Add (anchor.TrustedCert);
785+
}
786+
787+
var parameters = new PkixBuilderParameters (issuerStore, selector) {
788+
ValidityModel = PkixParameters.PkixValidityModel,
789+
IsRevocationEnabled = false,
790+
Date = DateTime.UtcNow
791+
};
792+
parameters.AddStoreCert (userCertificateStore);
793+
794+
if (CheckCertificateRevocation) {
795+
foreach (var certificate in chain)
796+
DownloadCrls (certificate, cancellationToken);
797+
}
798+
799+
var intermediateStore = GetIntermediateCertificates ();
800+
801+
foreach (var intermediate in intermediateStore.EnumerateMatches (new X509CertStoreSelector ())) {
802+
anchorStore.Add (intermediate);
803+
if (CheckCertificateRevocation)
804+
DownloadCrls (intermediate, cancellationToken);
805+
}
806+
807+
parameters.AddStoreCert (anchorStore);
808+
809+
if (CheckCertificateRevocation)
810+
parameters.AddStoreCrl (GetCertificateRevocationLists ());
811+
812+
try {
813+
var builder = new PkixCertPathBuilder ();
814+
builder.Build (parameters);
815+
return true;
816+
} catch {
817+
return false;
818+
}
819+
}
820+
821+
/// <summary>
822+
/// Validate an S/MIME certificate chain.
823+
/// </summary>
824+
/// <remarks>
825+
/// <para>Asynchronously validates an S/MIME certificate chain.</para>
826+
/// <para>Downloads the CRLs for each certificate in the chain and then validates that the chain
827+
/// is both valid and that none of the certificates in the chain have been revoked or compromised
828+
/// in any way.</para>
829+
/// </remarks>
830+
/// <returns><c>true</c> if the certificate chain is valid; otherwise, <c>false</c>.</returns>
831+
/// <param name="chain">The S/MIME certificate chain.</param>
832+
/// <param name="dateTime">The date and time to use for validation.</param>
833+
/// <param name="cancellationToken">The cancellation token.</param>
834+
/// <exception cref="ArgumentNullException">
835+
/// <paramref name="chain"/> is <see langword="null"/>.
836+
/// </exception>
837+
/// <exception cref="ArgumentException">
838+
/// <paramref name="chain"/> is empty or contains a <see langword="null"/> certificate.
839+
/// </exception>
840+
public async Task<bool> ValidateCertificateChainAsync (X509CertificateChain chain, DateTime dateTime, CancellationToken cancellationToken = default)
841+
{
842+
if (chain == null)
843+
throw new ArgumentNullException (nameof (chain));
844+
845+
if (chain.Count == 0)
846+
throw new ArgumentException ("The certificate chain must contain at least one certificate.", nameof (chain));
847+
848+
if (chain.Any (certificate => certificate == null))
849+
throw new ArgumentException ("The certificate chain contains at least one null certificate.", nameof (chain));
850+
851+
var selector = new X509CertStoreSelector ();
852+
853+
var userCertificateStore = new X509CertificateStore ();
854+
userCertificateStore.AddRange (chain);
855+
856+
var issuerStore = GetTrustedAnchors ();
857+
var anchorStore = new X509CertificateStore ();
858+
859+
foreach (var anchor in issuerStore) {
860+
anchorStore.Add (anchor.TrustedCert);
861+
}
862+
863+
var parameters = new PkixBuilderParameters (issuerStore, selector) {
864+
ValidityModel = PkixParameters.PkixValidityModel,
865+
IsRevocationEnabled = false,
866+
Date = DateTime.UtcNow
867+
};
868+
parameters.AddStoreCert (userCertificateStore);
869+
870+
if (CheckCertificateRevocation) {
871+
foreach (var certificate in chain)
872+
await DownloadCrlsAsync (certificate, cancellationToken).ConfigureAwait (false);
873+
}
874+
875+
var intermediateStore = GetIntermediateCertificates ();
876+
877+
foreach (var intermediate in intermediateStore.EnumerateMatches (new X509CertStoreSelector ())) {
878+
anchorStore.Add (intermediate);
879+
if (CheckCertificateRevocation)
880+
await DownloadCrlsAsync (intermediate, cancellationToken).ConfigureAwait (false);
881+
}
882+
883+
parameters.AddStoreCert (anchorStore);
884+
885+
if (CheckCertificateRevocation)
886+
parameters.AddStoreCrl (GetCertificateRevocationLists ());
887+
888+
try {
889+
var builder = new PkixCertPathBuilder ();
890+
builder.Build (parameters);
891+
return true;
892+
} catch {
893+
return false;
894+
}
895+
}
896+
723897
PkixCertPath BuildCertPath (ISet<TrustAnchor> anchors, IStore<X509Certificate> certificates, IStore<X509Crl> crls, X509Certificate certificate, DateTime signingTime)
724898
{
725899
var selector = new X509CertStoreSelector {
@@ -995,7 +1169,7 @@ static IEnumerable<string> EnumerateCrlDistributionPointUrls (X509Certificate ce
9951169
}
9961170
}
9971171

998-
void DownloadCrls (X509Certificate certificate, CancellationToken cancellationToken)
1172+
void DownloadCrls (X509Certificate certificate, CancellationToken cancellationToken = default)
9991173
{
10001174
var nextUpdate = GetNextCertificateRevocationListUpdate (certificate.IssuerDN);
10011175
var now = DateTime.UtcNow;
@@ -1125,10 +1299,15 @@ DigitalSignatureCollection GetDigitalSignatures (CmsSignedDataParser parser, Can
11251299
}
11261300

11271301
var anchors = GetTrustedAnchors ();
1302+
var intermediates = GetIntermediateCertificates ();
11281303

11291304
if (CheckCertificateRevocation) {
11301305
foreach (var anchor in anchors)
11311306
DownloadCrls (anchor.TrustedCert, cancellationToken);
1307+
1308+
foreach (X509Certificate intermediate in intermediates.EnumerateMatches(new X509CertStoreSelector())) {
1309+
DownloadCrls (intermediate, cancellationToken);
1310+
}
11321311
}
11331312

11341313
try {
@@ -1179,10 +1358,15 @@ async Task<DigitalSignatureCollection> GetDigitalSignaturesAsync (CmsSignedDataP
11791358
}
11801359

11811360
var anchors = GetTrustedAnchors ();
1361+
var intermediates = GetIntermediateCertificates ();
11821362

11831363
if (CheckCertificateRevocation) {
11841364
foreach (var anchor in anchors)
11851365
await DownloadCrlsAsync (anchor.TrustedCert, cancellationToken).ConfigureAwait (false);
1366+
1367+
foreach (X509Certificate intermediate in intermediates.EnumerateMatches (new X509CertStoreSelector ())) {
1368+
await DownloadCrlsAsync (intermediate, cancellationToken);
1369+
}
11861370
}
11871371

11881372
try {

MimeKit/Cryptography/DefaultSecureMimeContext.cs

+10-11
Original file line numberDiff line numberDiff line change
@@ -432,20 +432,19 @@ protected override ISet<TrustAnchor> GetTrustedAnchors ()
432432
/// <returns>The intermediate certificates.</returns>
433433
protected override IStore<X509Certificate> GetIntermediateCertificates ()
434434
{
435-
//var intermediates = new X509CertificateStore ();
436-
//var selector = new X509CertStoreSelector ();
437-
//var keyUsage = new bool[9];
435+
var intermediates = new X509CertificateStore ();
436+
var selector = new X509CertStoreSelector ();
437+
var keyUsage = new bool[9];
438438

439-
//keyUsage[(int) X509KeyUsageBits.KeyCertSign] = true;
440-
//selector.KeyUsage = keyUsage;
439+
keyUsage[(int) X509KeyUsageBits.KeyCertSign] = true;
440+
selector.KeyUsage = keyUsage;
441441

442-
//foreach (var record in dbase.Find (selector, false, X509CertificateRecordFields.Certificate)) {
443-
// if (!record.Certificate.IsSelfSigned ())
444-
// intermediates.Add (record.Certificate);
445-
//}
442+
foreach (var record in dbase.Find (selector, false, X509CertificateRecordFields.Certificate)) {
443+
if (!record.Certificate.IsSelfSigned ())
444+
intermediates.Add (record.Certificate);
445+
}
446446

447-
//return intermediates;
448-
return dbase;
447+
return intermediates;
449448
}
450449

451450
/// <summary>

UnitTests/Cryptography/ApplicationPkcs7MimeTests.cs

+13
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,19 @@
3535
using MimeKit.Cryptography;
3636

3737
using BCX509Certificate = Org.BouncyCastle.X509.X509Certificate;
38+
using Org.BouncyCastle.Cms;
39+
using Org.BouncyCastle.Crypto;
40+
using Org.BouncyCastle.OpenSsl;
41+
42+
using Org.BouncyCastle.Crypto;
43+
using Org.BouncyCastle.OpenSsl;
44+
using Org.BouncyCastle.Security;
45+
using Org.BouncyCastle.X509;
46+
using Org.BouncyCastle.Pkcs;
47+
using Org.BouncyCastle.Cms;
48+
using Org.BouncyCastle.Crypto.Encodings;
49+
using Org.BouncyCastle.Crypto.Engines;
50+
using Org.BouncyCastle.Crypto.Parameters;
3851

3952
namespace UnitTests.Cryptography {
4053
[TestFixture]

0 commit comments

Comments
 (0)