-
Notifications
You must be signed in to change notification settings - Fork 5.5k
ocsp: add parsing utilities for ASN.1 OCSP responses #12307
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
Merged
ggreenway
merged 33 commits into
envoyproxy:master
from
daniel-goldstein:ocsp-response-parsing
Aug 11, 2020
Merged
Changes from 26 commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
513ebc3
ocsp: add parsing utilities for ASN.1 OCSP responses
daniel-goldstein 4336a8c
fix spelling
daniel-goldstein 3e7d7bc
clang_tidy fix and undo deletion in BUILD
daniel-goldstein 3d2aecc
long line fix
daniel-goldstein a077362
spell check
daniel-goldstein 94bf777
replace function not implemented in boringssl_fips
daniel-goldstein 23da926
fix spelling pedantic
daniel-goldstein 5f904ad
wrap tests in anonymous namespace
daniel-goldstein bc351d0
clean up raw pointers and unused signature fields
daniel-goldstein ab88172
remove unused produced_at field
daniel-goldstein 561bdd1
change raw bytes from std string to std vector
daniel-goldstein 33ab2bf
change octet strings to be byte vectors
daniel-goldstein ead48ce
warn about minimal verification of OCSP responses
daniel-goldstein 1ed6234
change parse generalized time to use result type
daniel-goldstein e97c9c3
convert parse optional and sequence of to return ParsingResult
daniel-goldstein 4e7dc5d
convert octet string parsing to use ParseResult
daniel-goldstein a3bd2f6
change skip methods to use ParsingResult
daniel-goldstein fe8589b
complete all asn1 utility methods moving to ParsingResult
daniel-goldstein 26f5915
skip cert status
daniel-goldstein 5731d84
format
daniel-goldstein 07afafc
update docs
daniel-goldstein 8a5ba3a
fix spelling
daniel-goldstein c69faf3
Merge branch 'master' into ocsp-response-parsing
daniel-goldstein 4f3d405
add envoy extension package to BUILD
daniel-goldstein 57b5ff4
remove envoy_package and replace with envoy_extension_package
daniel-goldstein bc880d3
removee envoy_package import
daniel-goldstein 51988e5
backtick most new words instead of adding to the dictionary
daniel-goldstein 6d5c9d5
change get optional to return an optional CBS
daniel-goldstein db03fb3
fix optional implementation
daniel-goldstein 1b441e5
Merge branch 'master' into ocsp-response-parsing
daniel-goldstein 7398b7b
add runtime flag for checking the ocsp validity window start time
daniel-goldstein 4cd7d3c
format
daniel-goldstein 1dcb401
change thisUpdate check to log a warning instead of throw
daniel-goldstein File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| load( | ||
| "//bazel:envoy_build_system.bzl", | ||
| "envoy_cc_library", | ||
| "envoy_extension_package", | ||
| ) | ||
|
|
||
| licenses(["notice"]) # Apache 2 | ||
|
|
||
| envoy_extension_package() | ||
|
|
||
| envoy_cc_library( | ||
| name = "ocsp_lib", | ||
| srcs = ["ocsp.cc"], | ||
| hdrs = ["ocsp.h"], | ||
| repository = "", | ||
| deps = [ | ||
| ":asn1_utility_lib", | ||
| "//include/envoy/common:time_interface", | ||
| "//include/envoy/ssl:context_config_interface", | ||
| "//source/extensions/transport_sockets/tls:utility_lib", | ||
| ], | ||
| ) | ||
|
|
||
| envoy_cc_library( | ||
| name = "asn1_utility_lib", | ||
| srcs = ["asn1_utility.cc"], | ||
| hdrs = ["asn1_utility.h"], | ||
| repository = "", | ||
| deps = [ | ||
| "//include/envoy/common:time_interface", | ||
| "//include/envoy/ssl:context_config_interface", | ||
| "//source/common/common:c_smart_ptr_lib", | ||
| ], | ||
| ) |
134 changes: 134 additions & 0 deletions
134
source/extensions/transport_sockets/tls/ocsp/asn1_utility.cc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| #include "extensions/transport_sockets/tls/ocsp/asn1_utility.h" | ||
|
|
||
| #include "common/common/c_smart_ptr.h" | ||
|
|
||
| #include "absl/strings/ascii.h" | ||
|
|
||
| namespace Envoy { | ||
| namespace Extensions { | ||
| namespace TransportSockets { | ||
| namespace Tls { | ||
| namespace Ocsp { | ||
|
|
||
| namespace { | ||
| // A type adapter since OPENSSL_free accepts void*. | ||
| void freeOpensslString(char* str) { OPENSSL_free(str); } | ||
|
|
||
| // ASN1_INTEGER is a type alias for ASN1_STRING. | ||
| // This static_cast is intentional to avoid the | ||
| // c-style cast performed in M_ASN1_INTEGER_free. | ||
| void freeAsn1Integer(ASN1_INTEGER* integer) { | ||
| ASN1_STRING_free(static_cast<ASN1_STRING*>(integer)); | ||
| } | ||
| } // namespace | ||
|
|
||
| absl::string_view Asn1Utility::cbsToString(CBS& cbs) { | ||
| auto str_head = reinterpret_cast<const char*>(CBS_data(&cbs)); | ||
| return {str_head, CBS_len(&cbs)}; | ||
| } | ||
|
|
||
| ParsingResult<bool> Asn1Utility::getOptional(CBS& cbs, CBS* data, unsigned tag) { | ||
| int is_present; | ||
| if (!CBS_get_optional_asn1(&cbs, data, &is_present, tag)) { | ||
| return "Failed to parse ASN.1 element tag"; | ||
| } | ||
|
|
||
| return static_cast<bool>(is_present); | ||
| } | ||
|
|
||
| ParsingResult<std::string> Asn1Utility::parseOid(CBS& cbs) { | ||
| CBS oid; | ||
| if (!CBS_get_asn1(&cbs, &oid, CBS_ASN1_OBJECT)) { | ||
| return absl::string_view("Input is not a well-formed ASN.1 OBJECT"); | ||
| } | ||
| CSmartPtr<char, freeOpensslString> oid_text(CBS_asn1_oid_to_text(&oid)); | ||
| if (oid_text == nullptr) { | ||
| return absl::string_view("Failed to parse oid"); | ||
| } | ||
|
|
||
| std::string oid_text_str(oid_text.get()); | ||
| return oid_text_str; | ||
| } | ||
|
|
||
| ParsingResult<Envoy::SystemTime> Asn1Utility::parseGeneralizedTime(CBS& cbs) { | ||
| CBS elem; | ||
| if (!CBS_get_asn1(&cbs, &elem, CBS_ASN1_GENERALIZEDTIME)) { | ||
| return "Input is not a well-formed ASN.1 GENERALIZEDTIME"; | ||
| } | ||
|
|
||
| auto time_str = cbsToString(elem); | ||
| // OCSP follows the RFC 5280 enforcement that GENERALIZEDTIME | ||
| // fields MUST be in UTC, so must be suffixed with a Z character. | ||
| // Local time or time differential, though a part of the ASN.1 | ||
| // GENERALIZEDTIME spec, are not supported. | ||
| // Reference: https://tools.ietf.org/html/rfc5280#section-4.1.2.5.2 | ||
| if (time_str.length() > 0 && absl::ascii_toupper(time_str.at(time_str.length() - 1)) != 'Z') { | ||
| return "GENERALIZEDTIME must be in UTC"; | ||
| } | ||
|
|
||
| absl::Time time; | ||
| auto utc_time_str = time_str.substr(0, time_str.length() - 1); | ||
| std::string parse_error; | ||
| if (!absl::ParseTime(GENERALIZED_TIME_FORMAT, utc_time_str, &time, &parse_error)) { | ||
| return "Error parsing string of GENERALIZEDTIME format"; | ||
| } | ||
| return absl::ToChronoTime(time); | ||
| } | ||
|
|
||
| // Performs the following conversions to go from bytestring to hex integer | ||
| // CBS -> ASN1_INTEGER -> BIGNUM -> String. | ||
| ParsingResult<std::string> Asn1Utility::parseInteger(CBS& cbs) { | ||
| CBS num; | ||
| if (!CBS_get_asn1(&cbs, &num, CBS_ASN1_INTEGER)) { | ||
| return absl::string_view("Input is not a well-formed ASN.1 INTEGER"); | ||
| } | ||
|
|
||
| auto head = CBS_data(&num); | ||
| CSmartPtr<ASN1_INTEGER, freeAsn1Integer> asn1_integer( | ||
| c2i_ASN1_INTEGER(nullptr, &head, CBS_len(&num))); | ||
| if (asn1_integer != nullptr) { | ||
| BIGNUM num_bn; | ||
| BN_init(&num_bn); | ||
| ASN1_INTEGER_to_BN(asn1_integer.get(), &num_bn); | ||
|
|
||
| CSmartPtr<char, freeOpensslString> char_hex_number(BN_bn2hex(&num_bn)); | ||
| BN_free(&num_bn); | ||
| if (char_hex_number != nullptr) { | ||
| std::string hex_number(char_hex_number.get()); | ||
| return hex_number; | ||
| } | ||
| } | ||
|
|
||
| return absl::string_view("Failed to parse ASN.1 INTEGER"); | ||
| } | ||
|
|
||
| ParsingResult<std::vector<uint8_t>> Asn1Utility::parseOctetString(CBS& cbs) { | ||
| CBS value; | ||
| if (!CBS_get_asn1(&cbs, &value, CBS_ASN1_OCTETSTRING)) { | ||
| return "Input is not a well-formed ASN.1 OCTETSTRING"; | ||
| } | ||
|
|
||
| auto data = reinterpret_cast<const uint8_t*>(CBS_data(&value)); | ||
| return std::vector<uint8_t>{data, data + CBS_len(&value)}; | ||
| } | ||
|
|
||
| ParsingResult<absl::monostate> Asn1Utility::skipOptional(CBS& cbs, unsigned tag) { | ||
| if (!CBS_get_optional_asn1(&cbs, nullptr, nullptr, tag)) { | ||
| return "Failed to parse ASN.1 element tag"; | ||
| } | ||
| return absl::monostate(); | ||
| } | ||
|
|
||
| ParsingResult<absl::monostate> Asn1Utility::skip(CBS& cbs, unsigned tag) { | ||
| if (!CBS_get_asn1(&cbs, nullptr, tag)) { | ||
| return "Failed to parse ASN.1 element"; | ||
| } | ||
|
|
||
| return absl::monostate(); | ||
| } | ||
|
|
||
| } // namespace Ocsp | ||
| } // namespace Tls | ||
| } // namespace TransportSockets | ||
| } // namespace Extensions | ||
| } // namespace Envoy | ||
218 changes: 218 additions & 0 deletions
218
source/extensions/transport_sockets/tls/ocsp/asn1_utility.h
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,218 @@ | ||
| #pragma once | ||
|
|
||
| #include <iomanip> | ||
| #include <sstream> | ||
| #include <vector> | ||
|
|
||
| #include "envoy/common/exception.h" | ||
| #include "envoy/common/time.h" | ||
|
|
||
| #include "common/common/assert.h" | ||
|
|
||
| #include "absl/types/optional.h" | ||
| #include "absl/types/variant.h" | ||
| #include "openssl/bn.h" | ||
| #include "openssl/bytestring.h" | ||
| #include "openssl/ssl.h" | ||
|
|
||
| namespace Envoy { | ||
| namespace Extensions { | ||
| namespace TransportSockets { | ||
| namespace Tls { | ||
| namespace Ocsp { | ||
|
|
||
| constexpr absl::string_view GENERALIZED_TIME_FORMAT = "%E4Y%m%d%H%M%S"; | ||
|
|
||
| /** | ||
| * The result of parsing an ASN.1 structure or an `absl::string_view` error | ||
| * description. | ||
| */ | ||
| template <typename T> using ParsingResult = absl::variant<T, absl::string_view>; | ||
|
|
||
| /** | ||
| * Construct a `T` from the data contained in the CBS&. Functions | ||
| * of this type must advance the input CBS& over the element. | ||
| */ | ||
| template <typename T> using Asn1ParsingFunc = std::function<ParsingResult<T>(CBS&)>; | ||
|
|
||
| /** | ||
| * Utility functions for parsing DER-encoded ASN.1 objects. | ||
| * This relies heavily on the 'openssl/bytestring' API which | ||
| * is BoringSSL's recommended interface for parsing DER-encoded | ||
| * ASN.1 data when there is not an existing wrapper. | ||
| * This is not a complete library for ASN.1 parsing and primarily | ||
| * serves as abstractions for the OCSP module, but can be | ||
| * extended and moved into a general utility to support parsing of | ||
| * additional ASN.1 objects. | ||
| * | ||
| * Each function adheres to the invariant that given a reference | ||
| * to a crypto bytestring (CBS&), it will parse the specified | ||
| * ASN.1 element and advance `cbs` over it. | ||
| * | ||
| * An exception is thrown if the bytestring is malformed or does | ||
|
daniel-goldstein marked this conversation as resolved.
Outdated
|
||
| * not match the specified ASN.1 object. The position | ||
| * of `cbs` is not reliable after an exception is thrown. | ||
| */ | ||
| class Asn1Utility { | ||
| public: | ||
| ~Asn1Utility() = default; | ||
|
|
||
| /** | ||
| * Extracts the full contents of `cbs` as a string. | ||
| * | ||
| * @param `cbs` a CBS& that refers to the current document position | ||
| * @returns absl::string_view containing the contents of `cbs` | ||
| */ | ||
| static absl::string_view cbsToString(CBS& cbs); | ||
|
|
||
| /** | ||
| * Parses all elements of an ASN.1 SEQUENCE OF. `parse_element` must | ||
| * advance its input CBS& over the entire element. | ||
| * | ||
| * @param cbs a CBS& that refers to an ASN.1 SEQUENCE OF object | ||
| * @param parse_element an Asn1ParsingFunc<T> used to parse each element | ||
| * @returns ParsingResult<std::vector<T>> containing the parsed elements of the sequence | ||
| * or an error string if `cbs` does not point to a well-formed | ||
| * SEQUENCE OF object. | ||
| */ | ||
| template <typename T> | ||
| static ParsingResult<std::vector<T>> parseSequenceOf(CBS& cbs, Asn1ParsingFunc<T> parse_element); | ||
|
|
||
| /** | ||
| * Checks if an explicitly tagged optional element of `tag` is present and | ||
| * if so parses its value with `parse_data`. If the element is not present, | ||
| * `cbs` is not advanced. | ||
| * | ||
| * @param cbs a CBS& that refers to the current document position | ||
| * @param parse_data an Asn1ParsingFunc<T> used to parse the data if present | ||
| * @return ParsingResult<absl::optional<T>> with a `T` if `cbs` is of the specified tag, | ||
| * nullopt if the element has a different tag, or an error string if parsing fails. | ||
| */ | ||
| template <typename T> | ||
| static ParsingResult<absl::optional<T>> parseOptional(CBS& cbs, Asn1ParsingFunc<T> parse_data, | ||
| unsigned tag); | ||
|
|
||
| /** | ||
| * Returns whether or not an element explicitly tagged with `tag` is present | ||
| * at `cbs`. If so, `cbs` is advanced over the optional and assigns | ||
| * `data` to the inner element, if `data` is not nullptr. | ||
| * If `cbs` does not contain `tag`, `cbs` remains at the same position. | ||
| * | ||
| * @param cbs a CBS& that refers to the current document position | ||
| * @param data a CBS* that is set to the contents of `cbs` if present | ||
| * @param an unsigned explicit tag indicating an optional value | ||
| * | ||
| * @returns ParsingResult<bool> whether `cbs` points to an element tagged with `tag` or | ||
| * an error string if parsing fails. | ||
| */ | ||
| static ParsingResult<bool> getOptional(CBS& cbs, CBS* data, unsigned tag); | ||
|
|
||
| /** | ||
| * @param cbs a CBS& that refers to an ASN.1 OBJECT IDENTIFIER element | ||
| * @returns ParsingResult<std::string> the OID encoded in `cbs` or an error | ||
| * string if `cbs` does not point to a well-formed OBJECT IDENTIFIER | ||
| */ | ||
| static ParsingResult<std::string> parseOid(CBS& cbs); | ||
|
|
||
| /** | ||
| * @param cbs a CBS& that refers to an ASN.1 GENERALIZEDTIME element | ||
| * @returns ParsingResult<Envoy::SystemTime> the UTC timestamp encoded in `cbs` | ||
| * or an error string if `cbs` does not point to a well-formed | ||
| * GENERALIZEDTIME | ||
| */ | ||
| static ParsingResult<Envoy::SystemTime> parseGeneralizedTime(CBS& cbs); | ||
|
|
||
| /** | ||
| * Parses an ASN.1 INTEGER type into its hex string representation. | ||
| * ASN.1 INTEGER types are arbitrary precision. | ||
| * If you're SURE the integer fits into a fixed-size int, | ||
| * use CBS_get_asn1_* functions for the given integer type instead. | ||
| * | ||
| * @param cbs a CBS& that refers to an ASN.1 INTEGER element | ||
| * @returns ParsingResult<std::string> a hex representation of the integer | ||
| * or an error string if `cbs` does not point to a well-formed INTEGER | ||
| */ | ||
| static ParsingResult<std::string> parseInteger(CBS& cbs); | ||
|
|
||
| /** | ||
| * @param cbs a CBS& that refers to an ASN.1 OCTETSTRING element | ||
| * @returns ParsingResult<std::vector<uint8_t>> the octets in `cbs` or | ||
| * an error string if `cbs` does not point to a well-formed OCTETSTRING | ||
| */ | ||
| static ParsingResult<std::vector<uint8_t>> parseOctetString(CBS& cbs); | ||
|
|
||
| /** | ||
| * Advance `cbs` over an ASN.1 value of the class `tag` if that | ||
| * value is present. Otherwise, `cbs` stays in the same position. | ||
| * | ||
| * @param cbs a CBS& that refers to the current document position | ||
| * @param tag the tag of the value to skip | ||
| * @returns ParsingResult<absl::monostate> a unit type denoting success | ||
| * or an error string if parsing fails. | ||
| */ | ||
| static ParsingResult<absl::monostate> skipOptional(CBS& cbs, unsigned tag); | ||
|
|
||
| /** | ||
| * Advance `cbs` over an ASN.1 value of the class `tag`. | ||
| * | ||
| * @param cbs a CBS& that refers to the current document position | ||
| * @param tag the tag of the value to skip | ||
| * @returns ParsingResult<absl::monostate> a unit type denoting success | ||
| * or an error string if parsing fails. | ||
| */ | ||
| static ParsingResult<absl::monostate> skip(CBS& cbs, unsigned tag); | ||
| }; | ||
|
|
||
| template <typename T> | ||
| ParsingResult<std::vector<T>> Asn1Utility::parseSequenceOf(CBS& cbs, | ||
| Asn1ParsingFunc<T> parse_element) { | ||
| CBS seq_elem; | ||
| std::vector<T> vec; | ||
|
|
||
| // Initialize seq_elem to first element in sequence. | ||
| if (!CBS_get_asn1(&cbs, &seq_elem, CBS_ASN1_SEQUENCE)) { | ||
| return "Expected sequence of ASN.1 elements."; | ||
| } | ||
|
|
||
| while (CBS_data(&seq_elem) < CBS_data(&cbs)) { | ||
| // parse_element MUST advance seq_elem | ||
| auto elem_res = parse_element(seq_elem); | ||
| if (absl::holds_alternative<T>(elem_res)) { | ||
| vec.push_back(absl::get<0>(elem_res)); | ||
| } else { | ||
| return absl::get<1>(elem_res); | ||
| } | ||
| } | ||
|
|
||
| RELEASE_ASSERT(CBS_data(&cbs) == CBS_data(&seq_elem), | ||
| "Sequence tag length must match actual length or element parsing would fail"); | ||
|
|
||
| return vec; | ||
| } | ||
|
|
||
| template <typename T> | ||
| ParsingResult<absl::optional<T>> Asn1Utility::parseOptional(CBS& cbs, Asn1ParsingFunc<T> parse_data, | ||
| unsigned tag) { | ||
| CBS data; | ||
| auto is_present = getOptional(cbs, &data, tag); | ||
|
|
||
| if (absl::holds_alternative<absl::string_view>(is_present)) { | ||
| return absl::get<absl::string_view>(is_present); | ||
| } | ||
|
|
||
| if (absl::get<bool>(is_present)) { | ||
| auto res = parse_data(data); | ||
| if (absl::holds_alternative<T>(res)) { | ||
| return absl::get<0>(res); | ||
| } | ||
| return absl::get<1>(res); | ||
| } | ||
|
|
||
| return absl::nullopt; | ||
| } | ||
|
|
||
| } // namespace Ocsp | ||
| } // namespace Tls | ||
| } // namespace TransportSockets | ||
| } // namespace Extensions | ||
| } // namespace Envoy | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.