Skip to content

Commit 6f703a8

Browse files
committed
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.
1 parent a9542c6 commit 6f703a8

File tree

8 files changed

+350
-32
lines changed

8 files changed

+350
-32
lines changed

MimeKit/Cryptography/BouncyCastleSecureMimeContext.cs

+36-10
Original file line numberDiff line numberDiff line change
@@ -694,20 +694,41 @@ X509Certificate GetCertificate (IStore<X509Certificate> store, SignerID signer)
694694
/// <returns>The certificate chain, including the specified certificate.</returns>
695695
protected IList<X509Certificate> BuildCertificateChain (X509Certificate certificate)
696696
{
697-
var selector = new X509CertStoreSelector {
698-
Certificate = certificate
699-
};
697+
var selector = new X509CertStoreSelector ();
700698

701-
var intermediates = new X509CertificateStore ();
702-
intermediates.Add (certificate);
699+
var userCertificateStore = new X509CertificateStore ();
700+
userCertificateStore.Add (certificate);
703701

704-
var parameters = new PkixBuilderParameters (GetTrustedAnchors (), selector) {
702+
var issurerStore = GetTrustedAnchors ();
703+
var anchorStore = new X509CertificateStore ();
704+
705+
foreach (var anchor in issurerStore) {
706+
anchorStore.Add (anchor.TrustedCert);
707+
}
708+
709+
var parameters = new PkixBuilderParameters (issurerStore, selector) {
705710
ValidityModel = PkixParameters.PkixValidityModel,
706-
IsRevocationEnabled = false,
711+
IsRevocationEnabled = CheckCertificateRevocation,
707712
Date = DateTime.UtcNow
708713
};
709-
parameters.AddStoreCert (intermediates);
710-
parameters.AddStoreCert (GetIntermediateCertificates ());
714+
parameters.AddStoreCert (userCertificateStore);
715+
716+
if (CheckCertificateRevocation) {
717+
DownloadCrls (certificate);
718+
}
719+
720+
var intermediateStore = GetIntermediateCertificates ();
721+
722+
foreach (var intermediate in intermediateStore.EnumerateMatches (new X509CertStoreSelector ())) {
723+
anchorStore.Add (intermediate);
724+
if (CheckCertificateRevocation)
725+
DownloadCrls (intermediate);
726+
}
727+
728+
parameters.AddStoreCert (anchorStore);
729+
730+
if (CheckCertificateRevocation)
731+
parameters.AddStoreCrl (GetCertificateRevocationLists ());
711732

712733
var builder = new PkixCertPathBuilder ();
713734
var result = builder.Build (parameters);
@@ -995,7 +1016,7 @@ static IEnumerable<string> EnumerateCrlDistributionPointUrls (X509Certificate ce
9951016
}
9961017
}
9971018

998-
void DownloadCrls (X509Certificate certificate, CancellationToken cancellationToken)
1019+
void DownloadCrls (X509Certificate certificate, CancellationToken cancellationToken = default)
9991020
{
10001021
var nextUpdate = GetNextCertificateRevocationListUpdate (certificate.IssuerDN);
10011022
var now = DateTime.UtcNow;
@@ -1125,10 +1146,15 @@ DigitalSignatureCollection GetDigitalSignatures (CmsSignedDataParser parser, Can
11251146
}
11261147

11271148
var anchors = GetTrustedAnchors ();
1149+
var intermediates = GetIntermediateCertificates ();
11281150

11291151
if (CheckCertificateRevocation) {
11301152
foreach (var anchor in anchors)
11311153
DownloadCrls (anchor.TrustedCert, cancellationToken);
1154+
1155+
foreach (X509Certificate intermediate in intermediates.EnumerateMatches(new X509CertStoreSelector())) {
1156+
DownloadCrls (intermediate, cancellationToken);
1157+
}
11321158
}
11331159

11341160
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/SecureMimeTests.cs

+206-9
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
//
2626

2727
using System.Net;
28+
using System.Reflection;
2829
using System.Text;
2930
using System.Security.Cryptography.X509Certificates;
3031

@@ -79,7 +80,7 @@ public abstract class SecureMimeTestsBase
7980
public const string ThunderbirdName = "[email protected]";
8081

8182
public static readonly string[] RelativeConfigFilePaths = {
82-
"certificate-authority.cfg", "intermediate1.cfg", "intermediate2.cfg", "dnsnames/smime.cfg", "dsa/smime.cfg", "ec/smime.cfg", "revoked/smime.cfg", "rsa/smime.cfg"
83+
"certificate-authority.cfg", "intermediate1.cfg", "intermediate2.cfg", "dnsnames/smime.cfg", "dsa/smime.cfg", "ec/smime.cfg", "nochain/smime.cfg", "revoked/smime.cfg", "rsa/smime.cfg"
8384
};
8485

8586
public static readonly string[] StartComCertificates = {
@@ -260,7 +261,7 @@ protected static Mock<HttpMessageHandler> CreateMockHttpMessageHandler ()
260261
};
261262

262263
var mockHttpMessageHandler = new Mock<HttpMessageHandler> (MockBehavior.Strict);
263-
264+
264265
for (int i = 0; i < CrlRequestUris.Length; i++) {
265266
var requestUri = CrlRequestUris[i];
266267
var response = responses[i];
@@ -343,11 +344,11 @@ protected void ImportTestCertificates (SecureMimeContext ctx)
343344
Assert.DoesNotThrow (() => ctx.Import (mimekitCertificate.FileName, "no.secret"));
344345
}
345346

346-
if (windows is null) {
347-
// Import the obsolete CRLs (we want the S/MIME context to download the current CRLs)
348-
foreach (var crl in ObsoleteCrls)
349-
ctx.Import (crl);
350-
}
347+
// if (windows is null) {
348+
// // Import the obsolete CRLs (we want the S/MIME context to download the current CRLs)
349+
// foreach (var crl in ObsoleteCrls)
350+
// ctx.Import (crl);
351+
// }
351352
}
352353

353354
protected SecureMimeTestsBase ()
@@ -3000,6 +3001,116 @@ protected async Task VerifyRevokedCertificateAsync (BouncyCastleSecureMimeContex
30003001
Assert.That (innerException.Message, Does.EndWith (", reason: keyCompromise"));
30013002
}
30023003
}
3004+
3005+
protected void VerifyCrlsResolvedWithBuildCertificateChain (BouncyCastleSecureMimeContext ctx,
3006+
Mock<HttpMessageHandler> mockHttpMessageHandler)
3007+
{
3008+
var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up signing..." };
3009+
var certificate = SupportedCertificates.Single(c => c.EmailAddress == "[email protected]");
3010+
3011+
var signer = new CmsSigner (certificate.FileName, "no.secret");
3012+
var multipart = MultipartSigned.Create (ctx, signer, body);
3013+
3014+
Assert.That (multipart.Count, Is.EqualTo (2), "The multipart/signed has an unexpected number of children.");
3015+
3016+
var protocol = multipart.ContentType.Parameters["protocol"];
3017+
Assert.That (protocol, Is.EqualTo (ctx.SignatureProtocol), "The multipart/signed protocol does not match.");
3018+
3019+
Assert.That (multipart[0], Is.InstanceOf<TextPart> (), "The first child is not a text part.");
3020+
Assert.That (multipart[1], Is.InstanceOf<ApplicationPkcs7Signature> (), "The second child is not a detached signature.");
3021+
3022+
var signatures = multipart.Verify (ctx);
3023+
Assert.That (signatures.Count, Is.EqualTo (1), "Verify returned an unexpected number of signatures.");
3024+
3025+
AssertCrlsRequested (mockHttpMessageHandler);
3026+
3027+
AssertValidSignatures (ctx, signatures);
3028+
}
3029+
3030+
protected void VerifyCrlsResolved (BouncyCastleSecureMimeContext ctx,
3031+
Mock<HttpMessageHandler> mockHttpMessageHandler)
3032+
{
3033+
var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up signing..." };
3034+
var certificate = SupportedCertificates.Single (c => c.EmailAddress == "[email protected]");
3035+
3036+
var signer = new CmsSigner (certificate.FileName, "no.secret");
3037+
var multipart = MultipartSigned.Create (ctx, signer, body);
3038+
3039+
Assert.That (multipart.Count, Is.EqualTo (2), "The multipart/signed has an unexpected number of children.");
3040+
3041+
var protocol = multipart.ContentType.Parameters["protocol"];
3042+
Assert.That (protocol, Is.EqualTo (ctx.SignatureProtocol), "The multipart/signed protocol does not match.");
3043+
3044+
Assert.That (multipart[0], Is.InstanceOf<TextPart> (), "The first child is not a text part.");
3045+
Assert.That (multipart[1], Is.InstanceOf<ApplicationPkcs7Signature> (), "The second child is not a detached signature.");
3046+
3047+
var signatures = multipart.Verify (ctx);
3048+
Assert.That (signatures.Count, Is.EqualTo (1), "Verify returned an unexpected number of signatures.");
3049+
3050+
AssertCrlsRequested (mockHttpMessageHandler);
3051+
3052+
AssertValidSignatures (ctx, signatures);
3053+
}
3054+
3055+
protected void VerifyCrlMissing (BouncyCastleSecureMimeContext ctx, string errorContent)
3056+
{
3057+
var body = new TextPart ("plain") { Text = "This is some cleartext that we'll end up signing..." };
3058+
var certificate = SupportedCertificates.Single (c => c.EmailAddress == "[email protected]");
3059+
3060+
var signer = new CmsSigner (certificate.FileName, "no.secret");
3061+
using var multipart = MultipartSigned.Create (ctx, signer, body);
3062+
3063+
Assert.That (multipart.Count, Is.EqualTo (2), "The multipart/signed has an unexpected number of children.");
3064+
3065+
var protocol = multipart.ContentType.Parameters["protocol"];
3066+
Assert.That (protocol, Is.EqualTo (ctx.SignatureProtocol), "The multipart/signed protocol does not match.");
3067+
3068+
Assert.That (multipart[0], Is.InstanceOf<TextPart> (), "The first child is not a text part.");
3069+
Assert.That (multipart[1], Is.InstanceOf<ApplicationPkcs7Signature> (), "The second child is not a detached signature.");
3070+
3071+
var signatures = multipart.Verify (ctx);
3072+
Assert.That (signatures.Count, Is.EqualTo (1), "Verify returned an unexpected number of signatures.");
3073+
3074+
var ex = Assert.Throws<DigitalSignatureVerifyException> (() => signatures.Single ().Verify ());
3075+
Assert.That (ex.Message, Does.StartWith ("Failed to verify digital signature chain: Certification path could not be validated."));
3076+
Assert.That (ex.InnerException?.Message, Does.StartWith ("Certification path could not be validated."));
3077+
Assert.That(ex.InnerException?.InnerException?.Message, Is.EquivalentTo ($"No CRLs found for issuer \"{errorContent}\""));
3078+
}
3079+
3080+
3081+
protected void VerifyMimeEncapsulatedSigningWithContext (BouncyCastleSecureMimeContext ctx,
3082+
Mock<HttpMessageHandler> mockHttpMessageHandler)
3083+
{
3084+
var cleartext = new TextPart ("plain") { Text = "This is some text that we'll end up signing..." };
3085+
3086+
3087+
var certificate = SupportedCertificates.Single (c => c.EmailAddress == "[email protected]");
3088+
3089+
var self = new MailboxAddress ("MimeKit UnitTests", certificate.EmailAddress);
3090+
var signed = ApplicationPkcs7Mime.Sign (ctx, self, DigestAlgorithm.Sha1, cleartext);
3091+
MimeEntity extracted;
3092+
3093+
Assert.That (signed.SecureMimeType, Is.EqualTo (SecureMimeType.SignedData), "S/MIME type did not match.");
3094+
3095+
var signatures = signed.Verify (ctx, out extracted);
3096+
3097+
Assert.That (extracted, Is.InstanceOf<TextPart> (), "Extracted part is not the expected type.");
3098+
Assert.That (((TextPart) extracted).Text, Is.EqualTo (cleartext.Text), "Extracted content is not the same as the original.");
3099+
3100+
Assert.That (signatures.Count, Is.EqualTo (1), "Verify returned an unexpected number of signatures.");
3101+
AssertValidSignatures (ctx, signatures);
3102+
3103+
using (var signedData = signed.Content.Open ()) {
3104+
using (var stream = ctx.Verify (signedData, out signatures))
3105+
extracted = MimeEntity.Load (stream);
3106+
3107+
Assert.That (extracted, Is.InstanceOf<TextPart> (), "Extracted part is not the expected type.");
3108+
Assert.That (((TextPart) extracted).Text, Is.EqualTo (cleartext.Text), "Extracted content is not the same as the original.");
3109+
3110+
Assert.That (signatures.Count, Is.EqualTo (1), "Verify returned an unexpected number of signatures.");
3111+
AssertValidSignatures (ctx, signatures);
3112+
}
3113+
}
30033114
}
30043115

30053116
[TestFixture]
@@ -3010,11 +3121,11 @@ class MyTemporarySecureMimeContext : TemporarySecureMimeContext
30103121
public readonly Mock<HttpMessageHandler> MockHttpMessageHandler;
30113122
readonly HttpClient client;
30123123

3013-
public MyTemporarySecureMimeContext () : base (new SecureRandom (new CryptoApiRandomGenerator ()))
3124+
public MyTemporarySecureMimeContext (Mock<HttpMessageHandler>? mockHttpMessageHandler = null) : base (new SecureRandom (new CryptoApiRandomGenerator ()))
30143125
{
30153126
CheckCertificateRevocation = false;
30163127

3017-
MockHttpMessageHandler = CreateMockHttpMessageHandler ();
3128+
MockHttpMessageHandler = mockHttpMessageHandler ?? CreateMockHttpMessageHandler ();
30183129
client = new HttpClient (MockHttpMessageHandler.Object);
30193130
}
30203131

@@ -3055,6 +3166,92 @@ public async Task TestVerifyRevokedCertificateAsync ()
30553166
await VerifyRevokedCertificateAsync (ctx, ctx.MockHttpMessageHandler);
30563167
}
30573168
}
3169+
3170+
[Test]
3171+
public void TestVerifyCrlsResolved ()
3172+
{
3173+
using (var ctx = new MyTemporarySecureMimeContext () { CheckCertificateRevocation = true }) {
3174+
ImportTestCertificates (ctx);
3175+
3176+
VerifyCrlsResolved (ctx, ctx.MockHttpMessageHandler);
3177+
}
3178+
}
3179+
3180+
[Test]
3181+
public void TestMissingRootCrl ()
3182+
{
3183+
var responses = new HttpResponseMessage[]
3184+
{
3185+
new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(CurrentCrls[1].GetEncoded()) },
3186+
new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(CurrentCrls[2].GetEncoded()) }
3187+
};
3188+
var crlUrlIndexes = new[] { 1, 2 };
3189+
var errorContent = RootCertificate.SubjectDN.ToString ();
3190+
3191+
VerifyCrlMissingTest (responses, crlUrlIndexes, errorContent);
3192+
}
3193+
3194+
[Test]
3195+
public void TestMissingPrimaryIntermediateCrl ()
3196+
{
3197+
var responses = new HttpResponseMessage[]
3198+
{
3199+
new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(CurrentCrls[0].GetEncoded()) },
3200+
new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(CurrentCrls[2].GetEncoded()) }
3201+
};
3202+
var crlUrlIndexes = new[] { 0, 2 };
3203+
var errorContent = IntermediateCertificate1.SubjectDN.ToString ();
3204+
3205+
VerifyCrlMissingTest (responses, crlUrlIndexes, errorContent);
3206+
}
3207+
3208+
[Test]
3209+
public void TestMissingSecondaryIntermediateCrl ()
3210+
{
3211+
var responses = new HttpResponseMessage[]
3212+
{
3213+
new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(CurrentCrls[0].GetEncoded()) },
3214+
new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(CurrentCrls[1].GetEncoded()) }
3215+
};
3216+
var crlUrlIndexes = new[] { 0, 1 };
3217+
var errorContent = IntermediateCertificate2.SubjectDN.ToString ();
3218+
3219+
VerifyCrlMissingTest (responses, crlUrlIndexes, errorContent);
3220+
}
3221+
3222+
private void VerifyCrlMissingTest (HttpResponseMessage[] responses, int[] crlUrlIndexes, string errorContent)
3223+
{
3224+
var mockHttpMessageHandler = new Mock<HttpMessageHandler> (MockBehavior.Strict);
3225+
3226+
for (int i = 0; i < crlUrlIndexes.Length; i++) {
3227+
var requestUri = CrlRequestUris[crlUrlIndexes[i]];
3228+
var response = responses[i];
3229+
3230+
mockHttpMessageHandler
3231+
.Protected ()
3232+
.Setup<Task<HttpResponseMessage>> (
3233+
"SendAsync",
3234+
ItExpr.Is<HttpRequestMessage> (m => m.Method == HttpMethod.Get && m.RequestUri == requestUri),
3235+
ItExpr.IsAny<CancellationToken> ())
3236+
.ReturnsAsync (response);
3237+
}
3238+
3239+
using (var ctx = new MyTemporarySecureMimeContext (mockHttpMessageHandler) { CheckCertificateRevocation = true }) {
3240+
ImportTestCertificates (ctx);
3241+
3242+
VerifyCrlMissing (ctx, errorContent);
3243+
}
3244+
}
3245+
3246+
[Test]
3247+
public virtual void TestVerifyCrlsResolvedWiithSecureMimeEncapsulatedSigningWithContext ()
3248+
{
3249+
using (var ctx = new MyTemporarySecureMimeContext () { CheckCertificateRevocation = true }) {
3250+
ImportTestCertificates (ctx);
3251+
3252+
VerifyMimeEncapsulatedSigningWithContext (ctx, ctx.MockHttpMessageHandler);
3253+
}
3254+
}
30583255
}
30593256

30603257
[TestFixture]

0 commit comments

Comments
 (0)