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

ocsp: add parsing utilities for ASN.1 OCSP responses #12307

Merged
merged 33 commits into from
Aug 11, 2020

Conversation

daniel-goldstein
Copy link
Contributor

Signed-off-by: Daniel Goldstein [email protected]

Commit Message: add parsing utilities for ASN.1 OCSP responses
Additional Description: This is the first step in implementing OCSP stapling as described here, adding the capability on top of BoringSSL to parse and validate DER-encoded OCSP responses. It extracts enough to determine the revocation status of a single SSL certificate and the window of time for which the OCSP response can be considered valid. OCSP extensions are not currently processed.
Risk Level: Low/Medium (unused but part of larger feature related to TLS)
Testing: unit tests that include testing on openssl-generated certs and ocsp responses. There should be only two lines that don't have test coverage and they are both a result of BoringSSL potentially returning a nullptr which I have not yet been able to trigger.
Docs Changes:
Release Notes: N/A

Signed-off-by: Daniel Goldstein <[email protected]>
Signed-off-by: Daniel Goldstein <[email protected]>
Signed-off-by: Daniel Goldstein <[email protected]>
Signed-off-by: Daniel Goldstein <[email protected]>
Signed-off-by: Daniel Goldstein <[email protected]>
@ggreenway
Copy link
Contributor

@PiotrSikora Can you take a first pass on this? You're the most knowledgable regarding parsing ASN.1.

@daniel-goldstein
Copy link
Contributor Author

@PiotrSikora @ggreenway Bumping this since it's been sitting green without a review for a few days.

Copy link
Contributor

@ggreenway ggreenway left a comment

Choose a reason for hiding this comment

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

Sorry for the delay in reviewing.

The code is nicely structured and commented.

/wait

source/extensions/transport_sockets/tls/ocsp/ocsp.cc Outdated Show resolved Hide resolved
source/extensions/transport_sockets/tls/ocsp/ocsp.cc Outdated Show resolved Hide resolved
source/extensions/transport_sockets/tls/ocsp/ocsp.cc Outdated Show resolved Hide resolved
source/extensions/transport_sockets/tls/ocsp/ocsp.cc Outdated Show resolved Hide resolved
@ggreenway
Copy link
Contributor

As a design alternative, can this parsing be done with the openssl/boringssl X509_* APIs? What would be the tradeoffs (if it's even possible)?

@ggreenway
Copy link
Contributor

As a design alternative, can this parsing be done with the openssl/boringssl X509_* APIs? What would be the tradeoffs (if it's even possible)?

I investigated my own question. Looks like OpenSSL has support for this, but BoringSSL (which we use) does not.

@daniel-goldstein
Copy link
Contributor Author

daniel-goldstein commented Jul 31, 2020

Thanks for the feedback! I left the TODOs because it seemed like the PR was too long as is, but am happy to lump them into this PR.

I investigated my own question. Looks like OpenSSL has support for this, but BoringSSL (which we use) does not.

Yes this is mainly making up for BoringSSL having removed the OCSP data structures, and recommending that clients work with the CBS API for ASN.1 parsing.

@ggreenway
Copy link
Contributor

I'm getting this whole feature paged back into my brain now.

The utilities here are for the purpose of validating that a config-provided OCSP staple, which Envoy will serve to downstream clients during TLS handshakes, is valid (not expired, etc), correct? If so, disregard my comment on checking the signature. I think the goal would be to prevent likely user config errors, not verify trust. Things like using an expired value, or one for the wrong cert (things which you're checking already). Is my understanding correct?

@daniel-goldstein
Copy link
Contributor Author

Yes, that's true, responses are only coming from configs. Should I instead leave a note explaining why we don't need to inspect the signature but how it should be verified if responses are fetched dynamically in the future?

@ggreenway
Copy link
Contributor

Yes, that's true, responses are only coming from configs. Should I instead leave a note explaining why we don't need to inspect the signature but how it should be verified if responses are fetched dynamically in the future?

Yes to leaving a comment, but even if we are fetching them directly, I don't think we need to verify it. The client will always verify it; all we're doing here is trying to prevent operator-error misconfigurations (like supplying an OCSP staple for the wrong cert).

@daniel-goldstein
Copy link
Contributor Author

daniel-goldstein commented Aug 3, 2020

Yes to leaving a comment, but even if we are fetching them directly, I don't think we need to verify it. The client will always verify it; all we're doing here is trying to prevent operator-error misconfigurations (like supplying an OCSP staple for the wrong cert).

In this case, should I remove/skip the signature-related fields since they won't be processed?

@ggreenway
Copy link
Contributor

Yes to leaving a comment, but even if we are fetching them directly, I don't think we need to verify it. The client will always verify it; all we're doing here is trying to prevent operator-error misconfigurations (like supplying an OCSP staple for the wrong cert).

In this case, should I remove/skip the signature-related fields since they won't be processed?

Yes, I think so, with commenting (as discussed) on why we're skipping them.

Also, please make it clear (through naming, comments, whatever) that this code is not intended for verifying an upstream OCSP staple from a server. That requires a different set of validations than what you're doing here.

@ggreenway
Copy link
Contributor

Thinking through the use-case more as we've been talking about it, I think you can probably do a minimal set of validations: maybe the serial number and hostname and valid time (although even for time, it's not clear what to do if it's not valid right now, or if it's going to become invalid soon (like within a few seconds). The rest of the fields can be skipped. The goal here is to help the operator not make common mistakes.

If you disagree, let's discuss :)

@daniel-goldstein
Copy link
Contributor Author

daniel-goldstein commented Aug 3, 2020

Thinking through the use-case more as we've been talking about it, I think you can probably do a minimal set of validations: maybe the serial number and hostname and valid time (although even for time, it's not clear what to do if it's not valid right now, or if it's going to become invalid soon (like within a few seconds). The rest of the fields can be skipped. The goal here is to help the operator not make common mistakes.

If you disagree, let's discuss :)

By hostname, do you mean the name of the certificate issuer? If so then I would agree, since that is technically necessary to uniquely identify the certificate. I am fine skipping other superfluous fields to keep things simple, namely:

  • The signature stuff for the response
  • When the response was produced
  • The hash of the issuer's public key
  • The actual revocation status of the certificate

I think the time period checking at config time is a reasonable assertion, specifically because the validity end time is optional in OCSP responses, and a missing end time implies the response is always "expired" and not suited for stapling. This seems like a good use-case to fail quickly and alert the user that they need to ensure the responses they are delivering to Envoy have a valid window.

Copy link
Contributor

@ggreenway ggreenway left a comment

Choose a reason for hiding this comment

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

Code looks good overall; a few minor things.

It still feels like we're parsing more of the OCSP response than I'd like, given that we're just acting as a pass-through, but I think that'll be easier to look at with the next PR to see how it's all used.

source/extensions/transport_sockets/tls/ocsp/ocsp.cc Outdated Show resolved Hide resolved
tools/spelling/spelling_dictionary.txt Outdated Show resolved Hide resolved
@daniel-goldstein
Copy link
Contributor Author

@PiotrSikora I think everything else is set here if you could take a look

@ggreenway ggreenway merged commit 49f6966 into envoyproxy:master Aug 11, 2020
@daniel-goldstein daniel-goldstein deleted the ocsp-response-parsing branch August 21, 2020 16:06
davidben added a commit to davidben/envoy that referenced this pull request Feb 22, 2022
PR envoyproxy#12307 added a handrolled OCSP parser, but handled the version
field incorrectly.

ASN.1 tags are more than just a number. They also have a class
(universal, context-specific, application, or private). Class zero is
"universal", which is for the built-in types like INTEGER or SEQUENCE.

A tag written like [1] is not universal but "context-specific". That
means one needs to write CBS_ASN1_CONTEXT_SPECIFIC | 1, not plain 1.
Plain 1 is BOOLEAN. ASN.1 TLVs additionally are either constructed
or primitive. Explicitly-tagged types are always constructed (they
contain other TLVs). The constructed bit is encoded alongside the tag,
so CBS/CBB treat it as part of the tag.

See https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/ for
further background. BoringSSL's CBS/CBB APIs assume familiarity with
ASN.1, so reviewers for future ASN.1 PRs in Envoy should read over it.

An OCSP response contains the following field:

      version              [0] EXPLICIT Version DEFAULT v1,

With Version defined as:

      Version  ::=  INTEGER  {  v1(0) }

The outermost tag should be written as CBS_ASN1_CONTEXT_SPECIFIC |
CBS_ASN1_CONSTRUCTED | 0. ocsp.cc was using 0. This PR fixes it. (Other
tags in ocsp.cc appear to have been correct.) It also fixes the encoding
in the test to reflect the EXPLICIT tagging. The test was written as if
the version used IMPLICIT tagging.

The old code would not have successfully parsed any OCSP response with
an explicit version number. Likely the reason this wasn't noticed before
is because the field is DEFAULT v1 and v1 is the only defined version.
That means any OCSPv1 resolver is not only allowed to omit the version,
but *required* to do so under DER. As Envoy only implements v1, it
should be rejecting the field, rather than skipping it, but I've left
the (originally intended) behavior as-is for now.

Finally, this PR fixes some tests in asn1_utility_test.cc to use more
plausible test data. The test used tag [UNIVERSAL 0] rather than [0].
([0] is encoded 0xa0 when constructed.)

This PR is necessary to avoid Envoy breaking with future versions of
BoringSSL. Tag [UNIVERSAL 0] is particularly fraught because it is
reserved for use by the encoding. Notably, it is part of BER
indefinite-length EOC markers. Using it as an actual tag has some weird
conseequences and future versions of BoringSSL will no longer parse it.

Signed-off-by: David Benjamin <[email protected]>
ggreenway pushed a commit that referenced this pull request Feb 28, 2022
PR #12307 added a handrolled OCSP parser, but handled the version
field incorrectly.

ASN.1 tags are more than just a number. They also have a class
(universal, context-specific, application, or private). Class zero is
"universal", which is for the built-in types like INTEGER or SEQUENCE.

A tag written like [1] is not universal but "context-specific". That
means one needs to write CBS_ASN1_CONTEXT_SPECIFIC | 1, not plain 1.
Plain 1 is BOOLEAN. ASN.1 TLVs additionally are either constructed
or primitive. Explicitly-tagged types are always constructed (they
contain other TLVs). The constructed bit is encoded alongside the tag,
so CBS/CBB treat it as part of the tag.

See https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/ for
further background. BoringSSL's CBS/CBB APIs assume familiarity with
ASN.1, so reviewers for future ASN.1 PRs in Envoy should read over it.

An OCSP response contains the following field:

      version              [0] EXPLICIT Version DEFAULT v1,

With Version defined as:

      Version  ::=  INTEGER  {  v1(0) }

The outermost tag should be written as CBS_ASN1_CONTEXT_SPECIFIC |
CBS_ASN1_CONSTRUCTED | 0. ocsp.cc was using 0. This PR fixes it. (Other
tags in ocsp.cc appear to have been correct.) It also fixes the encoding
in the test to reflect the EXPLICIT tagging. The test was written as if
the version used IMPLICIT tagging.

The old code would not have successfully parsed any OCSP response with
an explicit version number. Likely the reason this wasn't noticed before
is because the field is DEFAULT v1 and v1 is the only defined version.
That means any OCSPv1 resolver is not only allowed to omit the version,
but *required* to do so under DER. As Envoy only implements v1, it
should be rejecting the field, rather than skipping it, but I've left
the (originally intended) behavior as-is for now.

Finally, this PR fixes some tests in asn1_utility_test.cc to use more
plausible test data. The test used tag [UNIVERSAL 0] rather than [0].
([0] is encoded 0xa0 when constructed.)

This PR is necessary to avoid Envoy breaking with future versions of
BoringSSL. Tag [UNIVERSAL 0] is particularly fraught because it is
reserved for use by the encoding. Notably, it is part of BER
indefinite-length EOC markers. Using it as an actual tag has some weird
conseequences and future versions of BoringSSL will no longer parse it.

Signed-off-by: David Benjamin <[email protected]>
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.

3 participants