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
Merged
Show file tree
Hide file tree
Changes from all 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 Jul 3, 2020
4336a8c
fix spelling
daniel-goldstein Jul 27, 2020
3e7d7bc
clang_tidy fix and undo deletion in BUILD
daniel-goldstein Jul 27, 2020
3d2aecc
long line fix
daniel-goldstein Jul 27, 2020
a077362
spell check
daniel-goldstein Jul 27, 2020
94bf777
replace function not implemented in boringssl_fips
daniel-goldstein Jul 28, 2020
23da926
fix spelling pedantic
daniel-goldstein Jul 28, 2020
5f904ad
wrap tests in anonymous namespace
daniel-goldstein Jul 28, 2020
bc351d0
clean up raw pointers and unused signature fields
daniel-goldstein Aug 3, 2020
ab88172
remove unused produced_at field
daniel-goldstein Aug 3, 2020
561bdd1
change raw bytes from std string to std vector
daniel-goldstein Aug 3, 2020
33ab2bf
change octet strings to be byte vectors
daniel-goldstein Aug 3, 2020
ead48ce
warn about minimal verification of OCSP responses
daniel-goldstein Aug 3, 2020
1ed6234
change parse generalized time to use result type
daniel-goldstein Aug 6, 2020
e97c9c3
convert parse optional and sequence of to return ParsingResult
daniel-goldstein Aug 6, 2020
4e7dc5d
convert octet string parsing to use ParseResult
daniel-goldstein Aug 6, 2020
a3bd2f6
change skip methods to use ParsingResult
daniel-goldstein Aug 6, 2020
fe8589b
complete all asn1 utility methods moving to ParsingResult
daniel-goldstein Aug 6, 2020
26f5915
skip cert status
daniel-goldstein Aug 6, 2020
5731d84
format
daniel-goldstein Aug 6, 2020
07afafc
update docs
daniel-goldstein Aug 6, 2020
8a5ba3a
fix spelling
daniel-goldstein Aug 6, 2020
c69faf3
Merge branch 'master' into ocsp-response-parsing
daniel-goldstein Aug 6, 2020
4f3d405
add envoy extension package to BUILD
daniel-goldstein Aug 6, 2020
57b5ff4
remove envoy_package and replace with envoy_extension_package
daniel-goldstein Aug 6, 2020
bc880d3
removee envoy_package import
daniel-goldstein Aug 6, 2020
51988e5
backtick most new words instead of adding to the dictionary
daniel-goldstein Aug 6, 2020
6d5c9d5
change get optional to return an optional CBS
daniel-goldstein Aug 7, 2020
db03fb3
fix optional implementation
daniel-goldstein Aug 10, 2020
1b441e5
Merge branch 'master' into ocsp-response-parsing
daniel-goldstein Aug 10, 2020
7398b7b
add runtime flag for checking the ocsp validity window start time
daniel-goldstein Aug 10, 2020
4cd7d3c
format
daniel-goldstein Aug 10, 2020
1dcb401
change thisUpdate check to log a warning instead of throw
daniel-goldstein Aug 10, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions source/extensions/transport_sockets/tls/ocsp/BUILD
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",
],
)
135 changes: 135 additions & 0 deletions source/extensions/transport_sockets/tls/ocsp/asn1_utility.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#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<absl::optional<CBS>> Asn1Utility::getOptional(CBS& cbs, unsigned tag) {
int is_present;
CBS data;
if (!CBS_get_optional_asn1(&cbs, &data, &is_present, tag)) {
return "Failed to parse ASN.1 element tag";
}

return is_present ? absl::optional(data) : absl::nullopt;
}

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
217 changes: 217 additions & 0 deletions source/extensions/transport_sockets/tls/ocsp/asn1_utility.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
#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
* 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 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<absl::optional<CBS>> getOptional(CBS& cbs, 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) {
auto maybe_data_res = getOptional(cbs, tag);

if (absl::holds_alternative<absl::string_view>(maybe_data_res)) {
return absl::get<absl::string_view>(maybe_data_res);
}

auto maybe_data = absl::get<absl::optional<CBS>>(maybe_data_res);
if (maybe_data) {
auto res = parse_data(maybe_data.value());
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
Loading