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

Add support for enforcing CRL expiration using nextUpdate field #227

Merged
merged 9 commits into from
May 16, 2024

Conversation

jasperpatterson
Copy link
Contributor

This adds support for enforcing the CRL nextUpdate field (i.e. expiration). Currently there is no way for a consumer of this crate to enforce expiration on a CRL since the nextUpdate field is not even exposed.

I considered simply exposing the next_update field on the CertRevocationList for a consumer to use on their own, but opted for an expiration enforcement option which seems more convenient, and follows the pattern of other builder options that already exist on RevocationOptionsBuilder.

I found this idea was also mentioned in one of the original PRs that added support for CRL:

There is no enforcement of the current time being greater than the CRL's NextUpdate. I found most implementations I surveyed chose not to treat this as a hard error so for now I've omitted any enforcement. Maybe this should be configurable?

To enforce expiration, you pass configuration to the RevocationOptionsBuilder:

RevocationOptionsBuilder::new(&crls)
	.unwrap()
	.with_expiration_policy(ExpirationPolicy::Enforce)
	.build();

The default behaviour, ignoring the nextUpdate field, remains unchanged.

Copy link

codecov bot commented Feb 7, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 97.25%. Comparing base (7fc65f1) to head (16d4b00).
Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #227      +/-   ##
==========================================
+ Coverage   97.21%   97.25%   +0.03%     
==========================================
  Files          19       19              
  Lines        4100     4158      +58     
==========================================
+ Hits         3986     4044      +58     
  Misses        114      114              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Member

@djc djc left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

The policy of ignoring by default feels at odds a little with our usual default of failing closed... Do we have any context of why other implementations aren't enforcing this?

@jasperpatterson for the clippy lint, probably just suppress it for that function?

@jasperpatterson
Copy link
Contributor Author

The policy of ignoring by default feels at odds a little with our usual default of failing closed... Do we have any context of why other implementations aren't enforcing this?

Personally, I agree. I expected this to be enforced and only caught that it wasn't because I wrote a test covering it.

The closest explanation I could find was this comment (about OCSP, not CRL) in the Go library: golang/go#45244 (comment)

I also was unsure about changing the default here since it could be a breaking change.

@djc
Copy link
Member

djc commented Feb 7, 2024

It might be good to look at what other implementations (notably BoringSSL and the Go std library) do for this case?

Copy link
Member

@ctz ctz left a comment

Choose a reason for hiding this comment

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

I think this would benefit from some integration test cases to explore the actual behaviour we want:

  1. a single CRL authoritative for the certificate, and the verification epoch is past nextUpdate.
  2. two CRLs authoritative for the certificate, one past, one current. I think the code right now will select the first one, and ignore the other. That is not ideal in several ways: a) later updates to the CRL are ignored, b) it fixates on an expired CRL when a "better" one is available, c) the order of CRLs in our API is unspecified, I think.

I think maybe it would be a better idea to do the expiration inside authoritative() which would work correctly for (2), while falling to status_policy for rejecting the certificate.

src/crl/mod.rs Outdated Show resolved Hide resolved
@cpu
Copy link
Member

cpu commented Feb 7, 2024

It might be good to look at what other implementations (notably BoringSSL and the Go std library) do for this case?

AIUI the Go standard library only exposes creating CRLs and verifying their signatures, not processing them for revocation decisions so it's not a very helpful point of comparison.

I looked at https://github.com/grpc/grpc-go's "advancedtls" package as one data point back when I was starting this work and found that it did check the CRL next update to decide if a cached CRL was appropriate or not, but if it was expired and the on-disk copy wasn't updated, it would proceed using the expired CRL. I emailed the maintainers at the time to see if they considered this a bug/security issue and they replied that it was by design saying:

Many implementations treat NextUpdate as an expiry, but we don't think this is always a good choice. If you choose to fail-open, an "old" CRL should be more secure than no CRL. And if you fail-closed, an "old" CRL would mean you reject everything, which is certainly secure but probably not desirable in many cases.

After that I compared with other implementations and found they acted similar and I replied:

Reading through BoringSSL's CRL processing code I believe it doesn't hard fail on expired CRLs and instead uses the validity as one heuristic input for a score used to determine the appropriateness of consulting the CRL. It's tricky from my cursory scan to determine if expiration alone would ever tip the scales but seems closer to your interpretation.

The handling within s2n is clearer and appears to explicitly eat the X509_V_ERR_CRL_HAS_EXPIRED error, returning 1 from the X509_STORE_CTX_set_verify_cb.

@jasperpatterson
Copy link
Contributor Author

two CRLs authoritative for the certificate, one past, one current. I think the code right now will select the first one, and ignore the other. That is not ideal in several ways: a) later updates to the CRL are ignored, b) it fixates on an expired CRL when a "better" one is available, c) the order of CRLs in our API is unspecified, I think.

You're right, though this kinda feels like an independent issue to me. It already wouldn't be picking the "best" CRL, regardless of expiration enforcement. e.g. It doesn't choose the most recently issued one.

Maybe filtering them based on the thisUpdate time and choosing the most recent could be an option?

@jasperpatterson
Copy link
Contributor Author

Just wanted to clarify what changes you all would be looking for on this PR in order to move it forward?

@cpu
Copy link
Member

cpu commented Apr 8, 2024

Just wanted to clarify what changes you all would be looking for on this PR in order to move it forward?

@jasperpatterson Apologies for the delay responding, I had missed the GitHub notification for this comment.

From my perspective I think this mainly needs the integration test coverage that @ctz mentioned in his earlier comment.

Beyond that, it would be helpful to see a draft PR on the Rustls repo that updates the ServerCertVerifierBuilder and ClientCertVerifierBuilder to use the new option implemented here. It would be nice to see this working end-to-end before we lock in the webpki side.

@cpu
Copy link
Member

cpu commented Apr 25, 2024

@jasperpatterson Is this ready for review after the latest push or should we hold off for a draft PR in Rustls, or further adjustments here?

@jasperpatterson
Copy link
Contributor Author

@cpu This is now ready for review!

I added in the integration tests as requested. That caused quite a large diff unfortunately, as I needed to force regenerate all the test CRLs and signatures, otherwise my new tests would fail with InvalidCrlSignatureForPublicKey.

I've also opened a draft PR in rustls: rustls/rustls#1922

@cpu
Copy link
Member

cpu commented Apr 25, 2024

@cpu This is now ready for review!

Awesome, thanks for iterating on this. I will review ASAP.

That caused quite a large diff unfortunately, as I needed to force regenerate all the test CRLs and signatures, otherwise my new tests would fail with InvalidCrlSignatureForPublicKey.

NP, that's expected in this case.

Copy link
Member

@cpu cpu left a comment

Choose a reason for hiding this comment

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

Looks good to me. Thank you for following up with the extra test coverage.

I had some nits/minor comments (and I think the tests/signatures/ changes need to be backed out) but the overall implementation seems correct.

src/crl/types.rs Outdated Show resolved Hide resolved
src/crl/types.rs Outdated Show resolved Hide resolved
tests/generate.py Outdated Show resolved Hide resolved
tests/generate.py Outdated Show resolved Hide resolved
Copy link
Member

@djc djc left a comment

Choose a reason for hiding this comment

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

LGTM (I did not review the integration tests).

src/verify_cert.rs Outdated Show resolved Hide resolved
@cpu cpu requested a review from ctz May 1, 2024 18:35
@cpu
Copy link
Member

cpu commented May 1, 2024

@ctz I think this is ready for you to give a review pass when you have a chance.

Copy link
Member

@ctz ctz left a comment

Choose a reason for hiding this comment

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

Thanks!

@cpu cpu added this pull request to the merge queue May 16, 2024
Merged via the queue into rustls:main with commit 3071d7b May 16, 2024
30 checks passed
@cpu
Copy link
Member

cpu commented May 16, 2024

@jasperpatterson Thanks for your patience with this one 🙇 I'm preparing a release with this changeset in #256

@jasperpatterson jasperpatterson deleted the jasper/crl-expiration branch May 16, 2024 18:45
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.

4 participants