Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include Intermediate Certificate Resolution for S/MIME and Ensure Trust Validation during BuildCertificateChain() #1124

Merged
merged 3 commits into from
Jan 1, 2025

Conversation

JoeShook
Copy link
Contributor

  • Added functionality to resolve intermediate certificates when building an S/MIME, ensuring trust validation.
  • Implemented tests to verify that Certificate Revocation Lists (CRLs) are resolved during S/MIME creation.
  • Introduced a new "nochain" certificate that lacks the complete certificate chain, reflecting a common scenario where certificates without the full chain are installed.
  • Enhanced logic to retrieve intermediates from the store and download CRLs, ensuring these processes are exercised and validated.

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.
ValidityModel = PkixParameters.PkixValidityModel,
IsRevocationEnabled = false,
IsRevocationEnabled = CheckCertificateRevocation,
Date = DateTime.UtcNow
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea of BuildCertificateChain wasn't to validate the chain, it was only to build it.

IMHO, if you really think we need to validate the chain when signing a message, then we should probably do it in the Sign/EncapsulatedSign methods, this way we can sync-vs-async download the CRLs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we should validate during the signing process. This option is essential for my workflows. One of the key advantages of SMIME is the ability to revoke a bad actor at scale. While this might not be as evident in scenarios where certificates are exchanged manually, it makes a lot of sense in my environment where the public certificate is discoverable via DNS.

I fully expected you to push back on the missing async paths.

I just got to a place where you could see the theme of the PR and tests to support it. I am used to writing the chain validation with System.Security.Cryptography. And there is just a lot going on in here to get familiar with. The thing I like about the BouncyCastle technique is I can control the caching. where with the MS stack on Windows it puts discovered intermediates in the machine store and crls in the file system and caches in memory. You can control the machine store and file system but you have to restart the service to clear the in memory cache. Maybe there is a PInvoke method that could punt the cache but I am not sure.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this might not be as evident in scenarios where certificates are exchanged manually, it makes a lot of sense in my environment where the public certificate is discoverable via DNS.

Are you signing with a certificate & private key discovered via DNS?

I guess that's unexpected for me.

When I think of signing a message, I think: Okay, the user is about to sign with his/her own private key that they 100% control and so they would know if it is invalid (except, I suppose, if it was invalid due to expiration perhaps). But I imagined they would know if they had revoked it, at least.

When encrypting to recipient certificates, it makes more sense to me to validate the certificate chains because those recipients could have revoked certificates. And obviously it also makes sense to me to validate a chain when verifying a signed message that you received.


if (CheckCertificateRevocation) {
DownloadCrls (certificate);
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what we do not want in this method. If the user is calling an XyzAsync method, we want to make sure that all I/O goes through async channels.

anchorStore.Add (intermediate);
if (CheckCertificateRevocation)
DownloadCrls (intermediate);
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

@jstedfast
Copy link
Owner

Perhaps these methods could be added to BouncyCastleSecureMimeContext and used in the Sign/SignAsync methods with CmsSigner.CertificateChain, like this:

if (CheckCertificateRevocation && !ValidateCertificateChain (signer.CertificateChain, DateTime.UtcNow, cancellationToken)
    throw new SomeSortOfException ();
/// <summary>
/// Validate an S/MIME certificate chain.
/// </summary>
/// <remarks>
/// <para>Validates an S/MIME certificate chain.</para>
/// <para>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.</para>
/// </remarks>
/// <returns><c>true</c> if the certificate chain is valid; otherwise, <c>false</c>.</returns>
/// <param name="chain">The S/MIME certificate chain.</param>
/// <param name="dateTime">The date and time to use for validation.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="ArgumentNullException">
/// <paramref name="chain"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="chain"/> is empty or contains a <see langword="null"/> certificate.
/// </exception>
public bool ValidateCertificateChain (X509Certificate[] chain, DateTime dateTime, CancellationToken cancellationToken = default)
{
	if (chain == null)
		throw new ArgumentNullException (nameof (chain));

	if (chain.Length == 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 {
		Certificate = chain[0]
	};

	var trustedAnchors = GetTrustedAnchors ();
	foreach (var anchor in trustedAnchors)
		DownloadCrls (anchor.TrustedCert, cancellationToken);

	var intermediates = new X509CertificateStore ();
	foreach (var certificate in chain) {
		DownloadCrls (certificate, cancellationToken);
		intermediates.Add (certificate);
	}

	var parameters = new PkixBuilderParameters (trustedAnchors, selector) {
		ValidityModel = PkixParameters.PkixValidityModel,
		IsRevocationEnabled = true,
		Date = dateTime
	};

	parameters.AddStoreCert (intermediates);
	parameters.AddStoreCrl (GetCertificateRevocationLists ());

	try {
		var builder = new PkixCertPathBuilder ();
		builder.Build (parameters);
		return true;
	} catch {
		return false;
	}
}

/// <summary>
/// Validate an S/MIME certificate chain.
/// </summary>
/// <remarks>
/// <para>Asynchronously validates an S/MIME certificate chain.</para>
/// <para>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.</para>
/// </remarks>
/// <returns><c>true</c> if the certificate chain is valid; otherwise, <c>false</c>.</returns>
/// <param name="chain">The S/MIME certificate chain.</param>
/// <param name="dateTime">The date and time to use for validation.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="ArgumentNullException">
/// <paramref name="chain"/> is <see langword="null"/>.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="chain"/> is empty or contains a <see langword="null"/> certificate.
/// </exception>
public async Task<bool> ValidateCertificateChainAsync (X509Certificate[] chain, DateTime dateTime, CancellationToken cancellationToken = default)
{
	if (chain == null)
		throw new ArgumentNullException (nameof (chain));

	if (chain.Length == 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 {
		Certificate = chain[0]
	};

	var trustedAnchors = GetTrustedAnchors ();
	foreach (var anchor in trustedAnchors)
		await DownloadCrlsAsync (anchor.TrustedCert, cancellationToken).ConfigureAwait (false);

	var intermediates = new X509CertificateStore ();
	foreach (var certificate in chain) {
		await DownloadCrlsAsync (certificate, cancellationToken).ConfigureAwait (false);
		intermediates.Add (certificate);
	}

	var parameters = new PkixBuilderParameters (trustedAnchors, selector) {
		ValidityModel = PkixParameters.PkixValidityModel,
		IsRevocationEnabled = true,
		Date = dateTime
	};

	parameters.AddStoreCert (intermediates);
	parameters.AddStoreCrl (GetCertificateRevocationLists ());

	try {
		var builder = new PkixCertPathBuilder ();
		builder.Build (parameters);
		return true;
	} catch {
		return false;
	}
}

@JoeShook
Copy link
Contributor Author

Probably won't look at this seriously until the end of the week or weekend. But all great feedback.

Thanks.

@jstedfast
Copy link
Owner

No worries. Have a Merry Christmas! 🎄🎁

Move the IO operations to ValidateCertificateChain and ValidateCertificateChainAsync.  Removed IO from BuildCertificateChain.
@jstedfast
Copy link
Owner

I'll try to review this today or tomorrow

ValidityModel = PkixParameters.PkixValidityModel,
IsRevocationEnabled = false,
IsRevocationEnabled = CheckCertificateRevocation,
Date = DateTime.UtcNow
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this might not be as evident in scenarios where certificates are exchanged manually, it makes a lot of sense in my environment where the public certificate is discoverable via DNS.

Are you signing with a certificate & private key discovered via DNS?

I guess that's unexpected for me.

When I think of signing a message, I think: Okay, the user is about to sign with his/her own private key that they 100% control and so they would know if it is invalid (except, I suppose, if it was invalid due to expiration perhaps). But I imagined they would know if they had revoked it, at least.

When encrypting to recipient certificates, it makes more sense to me to validate the certificate chains because those recipients could have revoked certificates. And obviously it also makes sense to me to validate a chain when verifying a signed message that you received.

@jstedfast jstedfast merged commit ea87b4d into jstedfast:master Jan 1, 2025
6 of 7 checks passed
@jstedfast
Copy link
Owner

Thanks for this work - the S/MIME stuff really needed some attention.

@JoeShook
Copy link
Contributor Author

JoeShook commented Jan 2, 2025

Are you signing with a certificate & private key discovered via DNS?

I guess that's unexpected for me.

When I think of signing a message, I think: Okay, the user is about to sign with his/her own private key that they 100% control and so they would know if it is invalid (except, I suppose, if it was invalid due to expiration perhaps). But I imagined they would know if they had revoked it, at least.

When encrypting to recipient certificates, it makes more sense to me to validate the certificate chains because those recipients could have revoked certificates. And obviously it also makes sense to me to validate a chain when verifying a signed message that you received.

Ugh! Everything you mentioned is spot on. I got so caught up in getting the trust part of the code to work that I lost track of the overall activity. I go tunnel vision on the trust mechanics and not on where it should be applied. We should probably revert this and reopen the PR.

We discover the public certificates for each recipient over DNS, and that's when trust must be established for each recipient. The work should be to ensure that trust is tested for each recipient in the Envelope method.

@jstedfast
Copy link
Owner

No sense reverting - we can just submit a new PR based on latest master to fix things.

@jstedfast
Copy link
Owner

I think this commit should fix things: f16687a

@JoeShook
Copy link
Contributor Author

JoeShook commented Jan 3, 2025

I will try to take a look over the weekend and test it out.

@jstedfast
Copy link
Owner

Would be good if you could verify that all of the cases you think need to be tested are tested as well. I got a little carried away removing some of your tests for signing, but then realized that your tests were based on tests I had added to test verifying revoked certificates used in the signature.

@jstedfast
Copy link
Owner

@JoeShook any updates?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants