diff --git a/source/extensions/transport_sockets/tls/ocsp/BUILD b/source/extensions/transport_sockets/tls/ocsp/BUILD new file mode 100644 index 0000000000000..e2995afd45cdf --- /dev/null +++ b/source/extensions/transport_sockets/tls/ocsp/BUILD @@ -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", + ], +) diff --git a/source/extensions/transport_sockets/tls/ocsp/asn1_utility.cc b/source/extensions/transport_sockets/tls/ocsp/asn1_utility.cc new file mode 100644 index 0000000000000..82cf430a7fd2b --- /dev/null +++ b/source/extensions/transport_sockets/tls/ocsp/asn1_utility.cc @@ -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(integer)); +} +} // namespace + +absl::string_view Asn1Utility::cbsToString(CBS& cbs) { + auto str_head = reinterpret_cast(CBS_data(&cbs)); + return {str_head, CBS_len(&cbs)}; +} + +ParsingResult> 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 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 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 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 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( + 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_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> 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(CBS_data(&value)); + return std::vector{data, data + CBS_len(&value)}; +} + +ParsingResult 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 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 diff --git a/source/extensions/transport_sockets/tls/ocsp/asn1_utility.h b/source/extensions/transport_sockets/tls/ocsp/asn1_utility.h new file mode 100644 index 0000000000000..05bea0c2d19b8 --- /dev/null +++ b/source/extensions/transport_sockets/tls/ocsp/asn1_utility.h @@ -0,0 +1,217 @@ +#pragma once + +#include +#include +#include + +#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 using ParsingResult = absl::variant; + +/** + * Construct a `T` from the data contained in the CBS&. Functions + * of this type must advance the input CBS& over the element. + */ +template using Asn1ParsingFunc = std::function(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` used to parse each element + * @returns ParsingResult> containing the parsed elements of the sequence + * or an error string if `cbs` does not point to a well-formed + * SEQUENCE OF object. + */ + template + static ParsingResult> parseSequenceOf(CBS& cbs, Asn1ParsingFunc 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` used to parse the data if present + * @return ParsingResult> 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 + static ParsingResult> parseOptional(CBS& cbs, Asn1ParsingFunc 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 whether `cbs` points to an element tagged with `tag` or + * an error string if parsing fails. + */ + static ParsingResult> getOptional(CBS& cbs, unsigned tag); + + /** + * @param cbs a CBS& that refers to an `ASN.1` OBJECT IDENTIFIER element + * @returns ParsingResult the `OID` encoded in `cbs` or an error + * string if `cbs` does not point to a well-formed OBJECT IDENTIFIER + */ + static ParsingResult parseOid(CBS& cbs); + + /** + * @param cbs a CBS& that refers to an `ASN.1` `GENERALIZEDTIME` element + * @returns ParsingResult the UTC timestamp encoded in `cbs` + * or an error string if `cbs` does not point to a well-formed + * `GENERALIZEDTIME` + */ + static ParsingResult 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 a hex representation of the integer + * or an error string if `cbs` does not point to a well-formed INTEGER + */ + static ParsingResult parseInteger(CBS& cbs); + + /** + * @param cbs a CBS& that refers to an `ASN.1` `OCTETSTRING` element + * @returns ParsingResult> the octets in `cbs` or + * an error string if `cbs` does not point to a well-formed `OCTETSTRING` + */ + static ParsingResult> 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` a unit type denoting success + * or an error string if parsing fails. + */ + static ParsingResult 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` a unit type denoting success + * or an error string if parsing fails. + */ + static ParsingResult skip(CBS& cbs, unsigned tag); +}; + +template +ParsingResult> Asn1Utility::parseSequenceOf(CBS& cbs, + Asn1ParsingFunc parse_element) { + CBS seq_elem; + std::vector 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(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 +ParsingResult> Asn1Utility::parseOptional(CBS& cbs, Asn1ParsingFunc parse_data, + unsigned tag) { + auto maybe_data_res = getOptional(cbs, tag); + + if (absl::holds_alternative(maybe_data_res)) { + return absl::get(maybe_data_res); + } + + auto maybe_data = absl::get>(maybe_data_res); + if (maybe_data) { + auto res = parse_data(maybe_data.value()); + if (absl::holds_alternative(res)) { + return absl::get<0>(res); + } + return absl::get<1>(res); + } + + return absl::nullopt; +} + +} // namespace Ocsp +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/transport_sockets/tls/ocsp/ocsp.cc b/source/extensions/transport_sockets/tls/ocsp/ocsp.cc new file mode 100644 index 0000000000000..11f7d0aa2c904 --- /dev/null +++ b/source/extensions/transport_sockets/tls/ocsp/ocsp.cc @@ -0,0 +1,303 @@ +#include "extensions/transport_sockets/tls/ocsp/ocsp.h" + +#include "common/common/utility.h" + +#include "extensions/transport_sockets/tls/ocsp/asn1_utility.h" +#include "extensions/transport_sockets/tls/utility.h" + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { +namespace Ocsp { + +namespace CertUtility = Envoy::Extensions::TransportSockets::Tls::Utility; + +namespace { + +template T unwrap(ParsingResult res) { + if (absl::holds_alternative(res)) { + return absl::get<0>(res); + } + + throw EnvoyException(std::string(absl::get<1>(res))); +} + +unsigned parseTag(CBS& cbs) { + unsigned tag; + if (!CBS_get_any_asn1_element(&cbs, nullptr, &tag, nullptr)) { + throw EnvoyException("Failed to parse ASN.1 element tag"); + } + return tag; +} + +std::unique_ptr readDerEncodedOcspResponse(const std::vector& der) { + CBS cbs; + CBS_init(&cbs, der.data(), der.size()); + + auto resp = Asn1OcspUtility::parseOcspResponse(cbs); + if (CBS_len(&cbs) != 0) { + throw EnvoyException("Data contained more than a single OCSP response"); + } + + return resp; +} + +void skipResponderId(CBS& cbs) { + // ResponderID ::= CHOICE { + // byName [1] Name, + // byKey [2] KeyHash + // } + // + // KeyHash ::= OCTET STRING -- SHA-1 hash of responder's public key + // (excluding the tag and length fields) + + if (unwrap(Asn1Utility::getOptional(cbs, CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 1)) || + unwrap(Asn1Utility::getOptional(cbs, CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 2))) { + return; + } + + throw EnvoyException(absl::StrCat("Unknown choice for Responder ID: ", parseTag(cbs))); +} + +void skipCertStatus(CBS& cbs) { + // CertStatus ::= CHOICE { + // good [0] IMPLICIT NULL, + // revoked [1] IMPLICIT RevokedInfo, + // unknown [2] IMPLICIT UnknownInfo + // } + if (!(unwrap(Asn1Utility::getOptional(cbs, CBS_ASN1_CONTEXT_SPECIFIC | 0)) || + unwrap( + Asn1Utility::getOptional(cbs, CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 1)) || + unwrap(Asn1Utility::getOptional(cbs, CBS_ASN1_CONTEXT_SPECIFIC | 2)))) { + throw EnvoyException(absl::StrCat("Unknown OcspCertStatus tag: ", parseTag(cbs))); + } +} + +} // namespace + +OcspResponse::OcspResponse(OcspResponseStatus status, ResponsePtr response) + : status_(status), response_(std::move(response)) {} + +BasicOcspResponse::BasicOcspResponse(ResponseData data) : data_(data) {} + +ResponseData::ResponseData(std::vector single_responses) + : single_responses_(std::move(single_responses)) {} + +SingleResponse::SingleResponse(CertId cert_id, Envoy::SystemTime this_update, + absl::optional next_update) + : cert_id_(cert_id), this_update_(this_update), next_update_(next_update) {} + +CertId::CertId(std::string serial_number) : serial_number_(serial_number) {} + +OcspResponseWrapper::OcspResponseWrapper(std::vector der_response, TimeSource& time_source) + : raw_bytes_(std::move(der_response)), response_(readDerEncodedOcspResponse(raw_bytes_)), + time_source_(time_source) { + + if (response_->response_ == nullptr) { + throw EnvoyException("OCSP response has no body"); + } + + // We only permit a 1:1 of certificate to response. + if (response_->response_->getNumCerts() != 1) { + throw EnvoyException("OCSP Response must be for one certificate only"); + } + + auto& this_update = response_->response_->getThisUpdate(); + if (time_source_.systemTime() < this_update) { + std::string time_format(GENERALIZED_TIME_FORMAT); + DateFormatter formatter(time_format); + ENVOY_LOG_MISC(warn, "OCSP Response thisUpdate field is set in the future: {}", + formatter.fromTime(this_update)); + } +} + +// We use just the serial number to uniquely identify a certificate. +// Though different issuers could produce certificates with the same serial +// number, this is check is to prevent operator error and a collision in this +// case is unlikely. +bool OcspResponseWrapper::matchesCertificate(X509& cert) { + std::string cert_serial_number = CertUtility::getSerialNumberFromCertificate(cert); + std::string resp_cert_serial_number = response_->response_->getCertSerialNumber(); + return resp_cert_serial_number == cert_serial_number; +} + +bool OcspResponseWrapper::isExpired() { + auto& next_update = response_->response_->getNextUpdate(); + return next_update == absl::nullopt || next_update < time_source_.systemTime(); +} + +std::unique_ptr Asn1OcspUtility::parseOcspResponse(CBS& cbs) { + // OCSPResponse ::= SEQUENCE { + // responseStatus OCSPResponseStatus, + // responseBytes [0] EXPLICIT ResponseBytes OPTIONAL + // } + + CBS elem; + if (!CBS_get_asn1(&cbs, &elem, CBS_ASN1_SEQUENCE)) { + throw EnvoyException("OCSP Response is not a well-formed ASN.1 SEQUENCE"); + } + + OcspResponseStatus status = Asn1OcspUtility::parseResponseStatus(elem); + auto maybe_bytes = + unwrap(Asn1Utility::getOptional(elem, CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 0)); + ResponsePtr resp = nullptr; + if (maybe_bytes) { + resp = Asn1OcspUtility::parseResponseBytes(maybe_bytes.value()); + } + + return std::make_unique(status, std::move(resp)); +} + +OcspResponseStatus Asn1OcspUtility::parseResponseStatus(CBS& cbs) { + // OCSPResponseStatus ::= ENUMERATED { + // successful (0), -- Response has valid confirmations + // malformedRequest (1), -- Illegal confirmation request + // internalError (2), -- Internal error in issuer + // tryLater (3), -- Try again later + // -- (4) is not used + // sigRequired (5), -- Must sign the request + // unauthorized (6) -- Request unauthorized + // } + CBS status; + if (!CBS_get_asn1(&cbs, &status, CBS_ASN1_ENUMERATED)) { + throw EnvoyException("OCSP ResponseStatus is not a well-formed ASN.1 ENUMERATED"); + } + + auto status_ordinal = *CBS_data(&status); + switch (status_ordinal) { + case 0: + return OcspResponseStatus::Successful; + case 1: + return OcspResponseStatus::MalformedRequest; + case 2: + return OcspResponseStatus::InternalError; + case 3: + return OcspResponseStatus::TryLater; + case 5: + return OcspResponseStatus::SigRequired; + case 6: + return OcspResponseStatus::Unauthorized; + default: + throw EnvoyException(absl::StrCat("Unknown OCSP Response Status variant: ", status_ordinal)); + } +} + +ResponsePtr Asn1OcspUtility::parseResponseBytes(CBS& cbs) { + // ResponseBytes ::= SEQUENCE { + // responseType RESPONSE. + // &id ({ResponseSet}), + // response OCTET STRING (CONTAINING RESPONSE. + // &Type({ResponseSet}{@responseType})) + // } + CBS elem, response; + if (!CBS_get_asn1(&cbs, &elem, CBS_ASN1_SEQUENCE)) { + throw EnvoyException("OCSP ResponseBytes is not a well-formed SEQUENCE"); + } + + auto oid_str = unwrap(Asn1Utility::parseOid(elem)); + if (!CBS_get_asn1(&elem, &response, CBS_ASN1_OCTETSTRING)) { + throw EnvoyException("Expected ASN.1 OCTETSTRING for response"); + } + + if (oid_str == BasicOcspResponse::OID) { + return Asn1OcspUtility::parseBasicOcspResponse(response); + } + throw EnvoyException(absl::StrCat("Unknown OCSP Response type with OID: ", oid_str)); +} + +std::unique_ptr Asn1OcspUtility::parseBasicOcspResponse(CBS& cbs) { + // BasicOCSPResponse ::= SEQUENCE { + // tbsResponseData ResponseData, + // signatureAlgorithm AlgorithmIdentifier{SIGNATURE-ALGORITHM, + // {`sa-dsaWithSHA1` | `sa-rsaWithSHA1` | + // `sa-rsaWithMD5` | `sa-rsaWithMD2`, ...}}, + // signature BIT STRING, + // certs [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL + // } + CBS elem; + if (!CBS_get_asn1(&cbs, &elem, CBS_ASN1_SEQUENCE)) { + throw EnvoyException("OCSP BasicOCSPResponse is not a wellf-formed ASN.1 SEQUENCE"); + } + auto response_data = Asn1OcspUtility::parseResponseData(elem); + // The `signatureAlgorithm` and `signature` are ignored because OCSP + // responses are expected to be delivered from a reliable source. + // Optional additional certs are ignored. + + return std::make_unique(response_data); +} + +ResponseData Asn1OcspUtility::parseResponseData(CBS& cbs) { + // ResponseData ::= SEQUENCE { + // version [0] EXPLICIT Version DEFAULT v1, + // responderID ResponderID, + // producedAt GeneralizedTime, + // responses SEQUENCE OF SingleResponse, + // responseExtensions [1] EXPLICIT Extensions OPTIONAL + // } + CBS elem; + if (!CBS_get_asn1(&cbs, &elem, CBS_ASN1_SEQUENCE)) { + throw EnvoyException("OCSP ResponseData is not a well-formed ASN.1 SEQUENCE"); + } + + unwrap(Asn1Utility::skipOptional(elem, 0)); + skipResponderId(elem); + unwrap(Asn1Utility::skip(elem, CBS_ASN1_GENERALIZEDTIME)); + auto responses = unwrap(Asn1Utility::parseSequenceOf( + elem, [](CBS& cbs) -> ParsingResult { + return ParsingResult(parseSingleResponse(cbs)); + })); + // Extensions currently ignored. + + return {std::move(responses)}; +} + +SingleResponse Asn1OcspUtility::parseSingleResponse(CBS& cbs) { + // SingleResponse ::= SEQUENCE { + // certID CertID, + // certStatus CertStatus, + // thisUpdate GeneralizedTime, + // nextUpdate [0] EXPLICIT GeneralizedTime OPTIONAL, + // singleExtensions [1] EXPLICIT Extensions OPTIONAL + // } + CBS elem; + if (!CBS_get_asn1(&cbs, &elem, CBS_ASN1_SEQUENCE)) { + throw EnvoyException("OCSP SingleResponse is not a well-formed ASN.1 SEQUENCE"); + } + + auto cert_id = Asn1OcspUtility::parseCertId(elem); + skipCertStatus(elem); + auto this_update = unwrap(Asn1Utility::parseGeneralizedTime(elem)); + auto next_update = unwrap(Asn1Utility::parseOptional( + elem, Asn1Utility::parseGeneralizedTime, + CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 0)); + // Extensions currently ignored. + + return {cert_id, this_update, next_update}; +} + +CertId Asn1OcspUtility::parseCertId(CBS& cbs) { + // CertID ::= SEQUENCE { + // hashAlgorithm AlgorithmIdentifier, + // issuerNameHash OCTET STRING, -- Hash of issuer's `DN` + // issuerKeyHash OCTET STRING, -- Hash of issuer's public key + // serialNumber CertificateSerialNumber + // } + CBS elem; + if (!CBS_get_asn1(&cbs, &elem, CBS_ASN1_SEQUENCE)) { + throw EnvoyException("OCSP CertID is not a well-formed ASN.1 SEQUENCE"); + } + + unwrap(Asn1Utility::skip(elem, CBS_ASN1_SEQUENCE)); + unwrap(Asn1Utility::skip(elem, CBS_ASN1_OCTETSTRING)); + unwrap(Asn1Utility::skip(elem, CBS_ASN1_OCTETSTRING)); + auto serial_number = unwrap(Asn1Utility::parseInteger(elem)); + + return {serial_number}; +} + +} // namespace Ocsp +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/transport_sockets/tls/ocsp/ocsp.h b/source/extensions/transport_sockets/tls/ocsp/ocsp.h new file mode 100644 index 0000000000000..35c274ee1579c --- /dev/null +++ b/source/extensions/transport_sockets/tls/ocsp/ocsp.h @@ -0,0 +1,293 @@ +#pragma once + +#include +#include +#include + +#include "envoy/common/exception.h" +#include "envoy/common/time.h" + +#include "absl/strings/string_view.h" +#include "absl/types/optional.h" +#include "openssl/bytestring.h" +#include "openssl/ssl.h" + +/** + * Data structures and functions for unmarshaling OCSP responses + * according to the RFC6960 B.2 spec. See: https://tools.ietf.org/html/rfc6960#appendix-B + * + * WARNING: This module is meant to validate that OCSP responses are well-formed + * and extract useful fields for OCSP stapling. This assumes that responses are + * provided from configs or another trusted source and does not perform + * checks necessary to verify responses coming from an upstream server. + */ + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { +namespace Ocsp { + +/** + * Reflection of the `ASN.1` OcspResponseStatus enumeration. + * The possible statuses that can accompany an OCSP response. + */ +enum class OcspResponseStatus { + // OCSPResponseStatus ::= ENUMERATED { + // successful (0), -- Response has valid confirmations + // malformedRequest (1), -- Illegal confirmation request + // internalError (2), -- Internal error in issuer + // tryLater (3), -- Try again later + // -- (4) is not used + // sigRequired (5), -- Must sign the request + // unauthorized (6) -- Request unauthorized + // } + Successful = 0, + MalformedRequest = 1, + InternalError = 2, + TryLater = 3, + SigRequired = 5, + Unauthorized = 6 +}; + +/** + * Partial reflection of the `ASN.1` CertId structure. + * Contains the information to identify an SSL Certificate. + * Serial numbers are guaranteed to be + * unique per issuer but not necessarily universally. + */ +struct CertId { + CertId(std::string serial_number); + + std::string serial_number_; +}; + +/** + * Partial reflection of the `ASN.1` SingleResponse structure. + * Contains information about the OCSP status of a single certificate. + * An OCSP request may request the status of multiple certificates and + * therefore responses may contain multiple SingleResponses. + * + * this_update_ and next_update_ reflect the validity period for this response. + * If next_update_ is not present, the OCSP responder always has new information + * available. In this case the response would be considered immediately expired + * and invalid for stapling. + */ +struct SingleResponse { + SingleResponse(CertId cert_id, Envoy::SystemTime this_update, + absl::optional next_update); + + const CertId cert_id_; + const Envoy::SystemTime this_update_; + const absl::optional next_update_; +}; + +/** + * Partial reflection of the `ASN.1` ResponseData structure. + * Contains an OCSP response for each certificate in a given request + * as well as the time at which the response was produced. + */ +struct ResponseData { + ResponseData(std::vector single_responses); + + const std::vector single_responses_; +}; + +/** + * An abstract type for OCSP response formats. Which variant of `Response` is + * used in an `OcspResponse` is indicated by the structure's `OID`. + * + * Envoy enforces that OCSP responses must be for a single certificate + * only. The methods on this class extract the relevant information for the + * single certificate contained in the response. + */ +class Response { +public: + virtual ~Response() = default; + + /** + * @return The number of certs reported on by this response. + */ + virtual size_t getNumCerts() PURE; + + /** + * @return The serial number of the certificate. + */ + virtual const std::string& getCertSerialNumber() PURE; + + /** + * @return The beginning of the validity window for this response. + */ + virtual const Envoy::SystemTime& getThisUpdate() PURE; + + /** + * The time at which this response is considered to expire. If + * `nullopt`, then there is assumed to always be more up-to-date + * information available and the response is always considered expired. + * + * @return The end of the validity window for this response. + */ + virtual const absl::optional& getNextUpdate() PURE; +}; + +using ResponsePtr = std::unique_ptr; + +/** + * Reflection of the `ASN.1` BasicOcspResponse structure. + * Contains the full data of an OCSP response. + * Envoy enforces that OCSP responses contain a response for only + * a single certificate. + * + * BasicOcspResponse is the only supported Response type in RFC 6960. + */ +class BasicOcspResponse : public Response { +public: + BasicOcspResponse(ResponseData data); + + // Response + size_t getNumCerts() override { return data_.single_responses_.size(); } + const std::string& getCertSerialNumber() override { + return data_.single_responses_[0].cert_id_.serial_number_; + } + const Envoy::SystemTime& getThisUpdate() override { + return data_.single_responses_[0].this_update_; + } + const absl::optional& getNextUpdate() override { + return data_.single_responses_[0].next_update_; + } + + // Identified as `id-pkix-ocsp-basic` in + // https://tools.ietf.org/html/rfc6960#appendix-B.2 + constexpr static absl::string_view OID = "1.3.6.1.5.5.7.48.1.1"; + +private: + const ResponseData data_; +}; + +/** + * Reflection of the `ASN.1` OcspResponse structure. + * This is the top-level data structure for OCSP responses. + */ +struct OcspResponse { + OcspResponse(OcspResponseStatus status, ResponsePtr response); + + OcspResponseStatus status_; + ResponsePtr response_; +}; + +/** + * A wrapper used to own and query an OCSP response in DER-encoded format. + */ +class OcspResponseWrapper { +public: + OcspResponseWrapper(std::vector der_response, TimeSource& time_source); + + /** + * @return std::vector& a reference to the underlying bytestring representation + * of the OCSP response + */ + const std::vector& rawBytes() { return raw_bytes_; } + + /** + * @return OcspResponseStatus whether the OCSP response was successfully created + * or a status indicating an error in the OCSP process + */ + OcspResponseStatus getResponseStatus() { return response_->status_; } + + /** + * @param cert a X509& SSL certificate + * @returns bool whether this OCSP response contains the revocation status of `cert` + */ + bool matchesCertificate(X509& cert); + + /** + * Determines whether the OCSP response can no longer be considered valid. + * This can be true if the nextUpdate field of the response has passed + * or is not present, indicating that there is always more updated information + * available. + * + * @returns bool if the OCSP response is expired. + */ + bool isExpired(); + +private: + const std::vector raw_bytes_; + const std::unique_ptr response_; + TimeSource& time_source_; +}; + +using OcspResponseWrapperPtr = std::unique_ptr; + +/** + * `ASN.1` DER-encoded parsing functions similar to `Asn1Utility` but specifically + * for structures related to OCSP. + * + * Each function must advance `cbs` across the element it refers to. + */ +class Asn1OcspUtility { +public: + /** + * @param `cbs` a CBS& that refers to an `ASN.1` OcspResponse element + * @returns std::unique_ptr the OCSP response encoded in `cbs` + * @throws Envoy::EnvoyException if `cbs` does not contain a well-formed OcspResponse + * element. + */ + static std::unique_ptr parseOcspResponse(CBS& cbs); + + /** + * @param cbs a CBS& that refers to an `ASN.1` OcspResponseStatus element + * @returns OcspResponseStatus the OCSP response encoded in `cbs` + * @throws Envoy::EnvoyException if `cbs` does not contain a well-formed + * OcspResponseStatus element. + */ + static OcspResponseStatus parseResponseStatus(CBS& cbs); + + /** + * @param cbs a CBS& that refers to an `ASN.1` Response element + * @returns Response containing the content of an OCSP response + * @throws Envoy::EnvoyException if `cbs` does not contain a well-formed + * structure that is a valid Response type. + */ + static ResponsePtr parseResponseBytes(CBS& cbs); + + /** + * @param cbs a CBS& that refers to an `ASN.1` BasicOcspResponse element + * @returns BasicOcspResponse containing the content of an OCSP response + * @throws Envoy::EnvoyException if `cbs` does not contain a well-formed + * BasicOcspResponse element. + */ + static std::unique_ptr parseBasicOcspResponse(CBS& cbs); + + /** + * @param cbs a CBS& that refers to an `ASN.1` ResponseData element + * @returns ResponseData containing the content of an OCSP response relating + * to certificate statuses. + * @throws Envoy::EnvoyException if `cbs` does not contain a well-formed + * ResponseData element. + */ + static ResponseData parseResponseData(CBS& cbs); + + /** + * @param cbs a CBS& that refers to an `ASN.1` SingleResponse element + * @returns SingleResponse containing the id and revocation status of + * a single certificate. + * @throws Envoy::EnvoyException if `cbs` does not contain a well-formed + * SingleResponse element. + */ + static SingleResponse parseSingleResponse(CBS& cbs); + + /** + * @param cbs a CBS& that refers to an `ASN.1` CertId element + * @returns CertId containing the information necessary to uniquely identify + * an SSL certificate. + * @throws Envoy::EnvoyException if `cbs` does not contain a well-formed + * CertId element. + */ + static CertId parseCertId(CBS& cbs); +}; + +} // namespace Ocsp +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/transport_sockets/tls/ocsp/BUILD b/test/extensions/transport_sockets/tls/ocsp/BUILD new file mode 100644 index 0000000000000..9f42f8d2ad056 --- /dev/null +++ b/test/extensions/transport_sockets/tls/ocsp/BUILD @@ -0,0 +1,41 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_test( + name = "ocsp_test", + srcs = [ + "ocsp_test.cc", + ], + data = [ + "gen_unittest_ocsp_data.sh", + ], + external_deps = ["ssl"], + deps = [ + "//source/common/filesystem:filesystem_lib", + "//source/extensions/transport_sockets/tls:utility_lib", + "//source/extensions/transport_sockets/tls/ocsp:ocsp_lib", + "//test/extensions/transport_sockets/tls:ssl_test_utils", + "//test/test_common:environment_lib", + "//test/test_common:logging_lib", + "//test/test_common:simulated_time_system_lib", + ], +) + +envoy_cc_test( + name = "asn1_utility_test", + srcs = [ + "asn1_utility_test.cc", + ], + external_deps = ["ssl"], + deps = [ + "//source/extensions/transport_sockets/tls/ocsp:asn1_utility_lib", + "//test/extensions/transport_sockets/tls:ssl_test_utils", + ], +) diff --git a/test/extensions/transport_sockets/tls/ocsp/asn1_utility_test.cc b/test/extensions/transport_sockets/tls/ocsp/asn1_utility_test.cc new file mode 100644 index 0000000000000..e3299c39bd22b --- /dev/null +++ b/test/extensions/transport_sockets/tls/ocsp/asn1_utility_test.cc @@ -0,0 +1,349 @@ +#include + +#include "extensions/transport_sockets/tls/ocsp/asn1_utility.h" + +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { +namespace Ocsp { + +namespace { + +class Asn1UtilityTest : public testing::Test { +public: + // DER encoding of a single TLV `ASN.1` element. + // returns a pointer to the underlying buffer and transfers + // ownership to the caller. + uint8_t* asn1Encode(CBS& cbs, std::string& value, unsigned tag) { + bssl::ScopedCBB cbb; + CBB child; + auto data_head = reinterpret_cast(value.c_str()); + + EXPECT_TRUE(CBB_init(cbb.get(), 0)); + EXPECT_TRUE(CBB_add_asn1(cbb.get(), &child, tag)); + EXPECT_TRUE(CBB_add_bytes(&child, data_head, value.size())); + + uint8_t* buf; + size_t buf_len; + EXPECT_TRUE(CBB_finish(cbb.get(), &buf, &buf_len)); + + CBS_init(&cbs, buf, buf_len); + return buf; + } + + template + void expectParseResultErrorOnWrongTag(std::function(CBS&)> parse) { + CBS cbs; + CBS_init(&cbs, asn1_true.data(), asn1_true.size()); + EXPECT_NO_THROW(absl::get<1>(parse(cbs))); + } + + const std::vector asn1_true = {0x1u, 1, 0xff}; + const std::vector asn1_empty_seq = {0x30, 0}; +}; + +TEST_F(Asn1UtilityTest, ParseMethodsWrongTagTest) { + expectParseResultErrorOnWrongTag>>([](CBS& cbs) { + return Asn1Utility::parseSequenceOf>(cbs, Asn1Utility::parseOctetString); + }); + expectParseResultErrorOnWrongTag(Asn1Utility::parseOid); + expectParseResultErrorOnWrongTag(Asn1Utility::parseGeneralizedTime); + expectParseResultErrorOnWrongTag(Asn1Utility::parseInteger); + expectParseResultErrorOnWrongTag>(Asn1Utility::parseOctetString); +} + +TEST_F(Asn1UtilityTest, ToStringTest) { + CBS cbs; + absl::string_view str = "test"; + CBS_init(&cbs, reinterpret_cast(str.data()), str.size()); + EXPECT_EQ(str, Asn1Utility::cbsToString(cbs)); +} + +TEST_F(Asn1UtilityTest, ParseSequenceOfEmptySequenceTest) { + CBS cbs; + CBS_init(&cbs, asn1_empty_seq.data(), asn1_empty_seq.size()); + + std::vector> vec; + auto actual = absl::get<0>( + Asn1Utility::parseSequenceOf>(cbs, Asn1Utility::parseOctetString)); + EXPECT_EQ(vec, actual); +} + +TEST_F(Asn1UtilityTest, ParseSequenceOfMultipleElementSequenceTest) { + std::vector octet_seq = { + // SEQUENCE OF 3 2-byte elements + 0x30, + 3 * (2 + 2), + // 1st OCTET STRING + 0x4u, + 2, + 0x1, + 0x2, + // 2nd OCTET STRING + 0x4u, + 2, + 0x3, + 0x4, + // 3rd OCTET STRING + 0x4u, + 2, + 0x5, + 0x6, + }; + CBS cbs; + CBS_init(&cbs, octet_seq.data(), octet_seq.size()); + + std::vector> vec = {{0x1, 0x2}, {0x3, 0x4}, {0x5, 0x6}}; + auto actual = absl::get<0>( + Asn1Utility::parseSequenceOf>(cbs, Asn1Utility::parseOctetString)); + EXPECT_EQ(vec, actual); +} + +TEST_F(Asn1UtilityTest, SequenceOfLengthMismatchErrorTest) { + std::vector malformed = { + // SEQUENCE OF length wrongfully 2 instead of 4 bytes + 0x30, + 3, + // 1st OCTET STRING + 0x4u, + 2, + 0x1, + 0x2, + }; + CBS cbs; + CBS_init(&cbs, malformed.data(), malformed.size()); + + EXPECT_EQ("Input is not a well-formed ASN.1 OCTETSTRING", + absl::get<1>(Asn1Utility::parseSequenceOf>( + cbs, Asn1Utility::parseOctetString))); +} + +TEST_F(Asn1UtilityTest, SequenceOfMixedTypeErrorTest) { + std::vector mixed_type = { + // SEQUENCE OF 1 OCTET STRING and 1 BOOLEAN + 0x30, + 7, + // OCTET STRING + 0x4u, + 2, + 0x1, + 0x2, + // BOOLEAN true + 0x1u, + 1, + 0xff, + }; + CBS cbs; + CBS_init(&cbs, mixed_type.data(), mixed_type.size()); + + EXPECT_EQ("Input is not a well-formed ASN.1 OCTETSTRING", + absl::get<1>(Asn1Utility::parseSequenceOf>( + cbs, Asn1Utility::parseOctetString))); +} + +TEST_F(Asn1UtilityTest, GetOptionalTest) { + CBS cbs; + CBS_init(&cbs, asn1_true.data(), asn1_true.size()); + + const uint8_t* start = CBS_data(&cbs); + EXPECT_EQ(absl::nullopt, absl::get<0>(Asn1Utility::getOptional(cbs, CBS_ASN1_INTEGER))); + EXPECT_EQ(start, CBS_data(&cbs)); + + CBS value = absl::get<0>(Asn1Utility::getOptional(cbs, CBS_ASN1_BOOLEAN)).value(); + EXPECT_EQ(0xff, *CBS_data(&value)); +} + +TEST_F(Asn1UtilityTest, GetOptionalMissingValueTest) { + std::vector missing_val_bool = {0x1u, 1}; + CBS cbs; + CBS_init(&cbs, missing_val_bool.data(), missing_val_bool.size()); + + auto res = Asn1Utility::getOptional(cbs, CBS_ASN1_BOOLEAN); + EXPECT_TRUE(absl::holds_alternative(res)); + EXPECT_EQ("Failed to parse ASN.1 element tag", absl::get<1>(res)); +} + +TEST_F(Asn1UtilityTest, ParseOptionalTest) { + std::vector nothing; + std::vector explicit_optional_true = {0, 3, 0x1u, 1, 0xff}; + + CBS cbs_true, cbs_explicit_optional_true, cbs_empty_seq, cbs_nothing; + CBS_init(&cbs_true, asn1_true.data(), asn1_true.size()); + CBS_init(&cbs_explicit_optional_true, explicit_optional_true.data(), + explicit_optional_true.size()); + CBS_init(&cbs_empty_seq, asn1_empty_seq.data(), asn1_empty_seq.size()); + CBS_init(&cbs_nothing, nothing.data(), nothing.size()); + + auto parseBool = [](CBS& cbs) -> bool { + int res; + CBS_get_asn1_bool(&cbs, &res); + return res; + }; + + absl::optional expected(true); + EXPECT_EQ(expected, absl::get<0>(Asn1Utility::parseOptional(cbs_explicit_optional_true, + parseBool, 0))); + EXPECT_EQ(absl::nullopt, absl::get<0>(Asn1Utility::parseOptional(cbs_empty_seq, parseBool, + CBS_ASN1_BOOLEAN))); + EXPECT_EQ(absl::nullopt, absl::get<0>(Asn1Utility::parseOptional(cbs_nothing, parseBool, + CBS_ASN1_BOOLEAN))); +} + +TEST_F(Asn1UtilityTest, ParseOidTest) { + std::string oid = "1.1.1.1.1.1.1"; + + bssl::ScopedCBB cbb; + CBB child; + ASSERT_TRUE(CBB_init(cbb.get(), 0)); + ASSERT_TRUE(CBB_add_asn1(cbb.get(), &child, CBS_ASN1_OBJECT)); + ASSERT_TRUE(CBB_add_asn1_oid_from_text(&child, oid.c_str(), oid.size())); + + uint8_t* buf; + size_t buf_len; + CBS cbs; + ASSERT_TRUE(CBB_finish(cbb.get(), &buf, &buf_len)); + CBS_init(&cbs, buf, buf_len); + bssl::UniquePtr scoped(buf); + + EXPECT_EQ(oid, absl::get<0>(Asn1Utility::parseOid(cbs))); +} + +TEST_F(Asn1UtilityTest, ParseGeneralizedTimeWrongFormatErrorTest) { + std::string invalid_time = ""; + CBS cbs; + bssl::UniquePtr scoped(asn1Encode(cbs, invalid_time, CBS_ASN1_GENERALIZEDTIME)); + Asn1Utility::parseGeneralizedTime(cbs); + EXPECT_EQ("Input is not a well-formed ASN.1 GENERALIZEDTIME", + absl::get(Asn1Utility::parseGeneralizedTime(cbs))); +} + +TEST_F(Asn1UtilityTest, ParseGeneralizedTimeTest) { + std::string time = "20070614185900z"; + std::string expected_time = "20070614185900"; + + CBS cbs; + bssl::UniquePtr scoped(asn1Encode(cbs, time, CBS_ASN1_GENERALIZEDTIME)); + absl::Time expected = TestUtility::parseTime(expected_time, "%E4Y%m%d%H%M%S"); + auto actual = absl::get(Asn1Utility::parseGeneralizedTime(cbs)); + + EXPECT_EQ(absl::ToChronoTime(expected), actual); +} + +TEST_F(Asn1UtilityTest, TestParseGeneralizedTimeRejectsNonUTCTime) { + std::string local_time = "20070601145918"; + CBS cbs; + bssl::UniquePtr scoped(asn1Encode(cbs, local_time, CBS_ASN1_GENERALIZEDTIME)); + + EXPECT_EQ("GENERALIZEDTIME must be in UTC", + absl::get(Asn1Utility::parseGeneralizedTime(cbs))); +} + +TEST_F(Asn1UtilityTest, TestParseGeneralizedTimeInvalidTime) { + std::string ymd = "20070601Z"; + CBS cbs; + bssl::UniquePtr scoped(asn1Encode(cbs, ymd, CBS_ASN1_GENERALIZEDTIME)); + + EXPECT_EQ("Error parsing string of GENERALIZEDTIME format", + absl::get<1>(Asn1Utility::parseGeneralizedTime(cbs))); +} + +// Taken from +// https://boringssl.googlesource.com/boringssl/+/master/crypto/bytestring/cbb.c#531 +// because boringssl_fips does not yet implement `CBB_add_asn1_int64` +void cbbAddAsn1Int64(CBB* cbb, int64_t value) { + if (value >= 0) { + ASSERT_TRUE(CBB_add_asn1_uint64(cbb, value)); + } + + union { + int64_t i; + uint8_t bytes[sizeof(int64_t)]; + } u; + u.i = value; + int start = 7; + // Skip leading sign-extension bytes unless they are necessary. + while (start > 0 && (u.bytes[start] == 0xff && (u.bytes[start - 1] & 0x80))) { + start--; + } + + CBB child; + ASSERT_TRUE(CBB_add_asn1(cbb, &child, CBS_ASN1_INTEGER)); + for (int i = start; i >= 0; i--) { + ASSERT_TRUE(CBB_add_u8(&child, u.bytes[i])); + } + CBB_flush(cbb); +} + +TEST_F(Asn1UtilityTest, ParseIntegerTest) { + std::vector> integers = { + {1, "01"}, + {10, "0a"}, + {1000000, "0f4240"}, + {-1, "-01"}, + }; + bssl::ScopedCBB cbb; + CBS cbs; + uint8_t* buf; + size_t buf_len; + for (auto const& int_and_hex : integers) { + ASSERT_TRUE(CBB_init(cbb.get(), 0)); + cbbAddAsn1Int64(cbb.get(), int_and_hex.first); + ASSERT_TRUE(CBB_finish(cbb.get(), &buf, &buf_len)); + + CBS_init(&cbs, buf, buf_len); + bssl::UniquePtr scoped_buf(buf); + + EXPECT_EQ(int_and_hex.second, absl::get<0>(Asn1Utility::parseInteger(cbs))); + cbb.Reset(); + } +} + +TEST_F(Asn1UtilityTest, ParseOctetStringTest) { + std::vector data = {0x1, 0x2, 0x3}; + std::string data_str(data.begin(), data.end()); + CBS cbs; + bssl::UniquePtr scoped(asn1Encode(cbs, data_str, CBS_ASN1_OCTETSTRING)); + + EXPECT_EQ(data, absl::get<0>(Asn1Utility::parseOctetString(cbs))); +} + +TEST_F(Asn1UtilityTest, SkipOptionalPresentAdvancesTest) { + CBS cbs; + CBS_init(&cbs, asn1_empty_seq.data(), asn1_empty_seq.size()); + + const uint8_t* start = CBS_data(&cbs); + EXPECT_NO_THROW(absl::get<0>(Asn1Utility::skipOptional(cbs, CBS_ASN1_SEQUENCE))); + EXPECT_EQ(start + 2, CBS_data(&cbs)); +} + +TEST_F(Asn1UtilityTest, SkipOptionalNotPresentDoesNotAdvanceTest) { + CBS cbs; + CBS_init(&cbs, asn1_empty_seq.data(), asn1_empty_seq.size()); + + const uint8_t* start = CBS_data(&cbs); + EXPECT_NO_THROW(absl::get<0>(Asn1Utility::skipOptional(cbs, CBS_ASN1_BOOLEAN))); + EXPECT_EQ(start, CBS_data(&cbs)); +} + +TEST_F(Asn1UtilityTest, SkipOptionalMalformedTagTest) { + std::vector malformed_seq = {0x30}; + CBS cbs; + CBS_init(&cbs, malformed_seq.data(), malformed_seq.size()); + + EXPECT_EQ("Failed to parse ASN.1 element tag", + absl::get<1>(Asn1Utility::skipOptional(cbs, CBS_ASN1_SEQUENCE))); +} + +} // namespace + +} // namespace Ocsp +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/transport_sockets/tls/ocsp/gen_unittest_ocsp_data.sh b/test/extensions/transport_sockets/tls/ocsp/gen_unittest_ocsp_data.sh new file mode 100755 index 0000000000000..a2a8f8b3a4e65 --- /dev/null +++ b/test/extensions/transport_sockets/tls/ocsp/gen_unittest_ocsp_data.sh @@ -0,0 +1,156 @@ +#!/bin/bash +# +# Create test certificates and OCSP responses for them for unittests. + +set -e + +trap cleanup EXIT +cleanup() { + rm *_index* + rm *.csr + rm *.cnf + rm *_serial* +} + +[[ -z "${TEST_TMPDIR}" ]] && TEST_TMPDIR="$(cd $(dirname $0) && pwd)" + +TEST_OCSP_DIR="${TEST_TMPDIR}/ocsp_test_data" +mkdir -p "${TEST_OCSP_DIR}" + +cd $TEST_OCSP_DIR + +################################################## +# Make the configuration file +################################################## + +# $1= $2= +generate_config() { +(cat << EOF +[ req ] +default_bits = 2048 +distinguished_name = req_distinguished_name + +[ req_distinguished_name ] +countryName = US +countryName_default = US +stateOrProvinceName = California +stateOrProvinceName_default = California +localityName = San Francisco +localityName_default = San Francisco +organizationName = Lyft +organizationName_default = Lyft +organizationalUnitName = Lyft Engineering +organizationalUnitName_default = Lyft Engineering +commonName = $1 +commonName_default = $1 +commonName_max = 64 + +[ ca ] +default_ca = CA_default + +[ CA_default ] +dir = ${TEST_OCSP_DIR} +certs = ${TEST_OCSP_DIR} +new_certs_dir = ${TEST_OCSP_DIR} +serial = ${TEST_OCSP_DIR} +database = ${TEST_OCSP_DIR}/$2_index.txt +serial = ${TEST_OCSP_DIR}/$2_serial + +private_key = ${TEST_OCSP_DIR}/$2_key.pem +certificate = ${TEST_OCSP_DIR}/$2_cert.pem + +default_days = 375 +default_md = sha256 +preserve = no +policy = policy_default + +[ policy_default ] +countryName = optional +stateOrProvinceName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + + +[ v3_ca ] +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always,issuer +basicConstraints = critical, CA:true +keyUsage = critical, digitalSignature, cRLSign, keyCertSign +EOF +) > $1.cnf +} + +# $1= $2=[issuer name] +generate_ca() { + if [[ "$2" != "" ]]; then local EXTRA_ARGS="-CA $2_cert.pem -CAkey $2_key.pem -CAcreateserial"; fi + openssl genrsa -out $1_key.pem 2048 + openssl req -new -key $1_key.pem -out $1_cert.csr \ + -config $1.cnf -batch -sha256 + openssl x509 -req \ + -in $1_cert.csr -signkey $1_key.pem -out $1_cert.pem \ + -extensions v3_ca -extfile $1.cnf $EXTRA_ARGS +} + +# $1= $2= +generate_x509_cert() { + openssl genrsa -out $1_key.pem 2048 + openssl req -new -key $1_key.pem -out $1_cert.csr -config $1.cnf -batch -sha256 + openssl ca -config $1.cnf -notext -batch -in $1_cert.csr -out $1_cert.pem +} + +# $1= $2= $3= $4=[extra args] +generate_ocsp_response() { + # Generate an OCSP request + openssl ocsp -CAfile $2_cert.pem -issuer $2_cert.pem \ + -cert $1_cert.pem -reqout $3_ocsp_req.der + + # Generate the OCSP response + openssl ocsp -CA $2_cert.pem \ + -rkey $2_key.pem -rsigner $2_cert.pem -index $2_index.txt \ + -reqin $3_ocsp_req.der -respout $3_ocsp_resp.der $4 +} + +# $1= $2= +revoke_certificate() { + openssl ca -revoke $1_cert.pem -keyfile $2_key.pem -cert $2_cert.pem -config $2.cnf +} + +# Set up the CA +touch ca_index.txt +echo "unique_subject = no" > ca_index.txt.attr +echo 1000 > ca_serial +generate_config ca ca +generate_ca ca + +# Set up an intermediate CA with a different database +touch intermediate_ca_index.txt +echo "unique_subject = no" > intermediate_ca_index.txt.attr +echo 1000 > intermediate_ca_serial +generate_config intermediate_ca intermediate_ca +generate_ca intermediate_ca ca + +# Generate valid cert and OCSP response +generate_config good ca +generate_x509_cert good ca +generate_ocsp_response good ca good "-ndays 7" + +# Generate OCSP response with the responder key hash instead of name +generate_ocsp_response good ca responder_key_hash -resp_key_id + +# Generate and revoke a cert and create OCSP response +generate_config revoked ca +generate_x509_cert revoked ca +revoke_certificate revoked ca +generate_ocsp_response revoked ca revoked + +# Create OCSP response for cert unknown to the CA +generate_ocsp_response good intermediate_ca unknown + +# Generate an OCSP request/response for multiple certs +openssl ocsp -CAfile ca_cert.pem -issuer ca_cert.pem \ + -cert good_cert.pem -cert revoked_cert.pem -reqout multiple_cert_ocsp_req.der +openssl ocsp -CA ca_cert.pem \ + -rkey ca_key.pem -rsigner ca_cert.pem -index ca_index.txt \ + -reqin multiple_cert_ocsp_req.der -respout multiple_cert_ocsp_resp.der diff --git a/test/extensions/transport_sockets/tls/ocsp/ocsp_test.cc b/test/extensions/transport_sockets/tls/ocsp/ocsp_test.cc new file mode 100644 index 0000000000000..aebca52c04ef2 --- /dev/null +++ b/test/extensions/transport_sockets/tls/ocsp/ocsp_test.cc @@ -0,0 +1,277 @@ +#include "common/filesystem/filesystem_impl.h" + +#include "extensions/transport_sockets/tls/ocsp/ocsp.h" +#include "extensions/transport_sockets/tls/utility.h" + +#include "test/extensions/transport_sockets/tls/ssl_test_utility.h" +#include "test/test_common/environment.h" +#include "test/test_common/logging.h" +#include "test/test_common/simulated_time_system.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "openssl/x509v3.h" + +namespace Envoy { +namespace Extensions { +namespace TransportSockets { +namespace Tls { +namespace Ocsp { + +namespace { + +namespace CertUtility = Envoy::Extensions::TransportSockets::Tls::Utility; + +class OcspFullResponseParsingTest : public testing::Test { +public: + static void SetUpTestSuite() { // NOLINT(readability-identifier-naming) + TestEnvironment::exec({TestEnvironment::runfilesPath( + "test/extensions/transport_sockets/tls/ocsp/gen_unittest_ocsp_data.sh")}); + } + + std::string fullPath(std::string filename) { + return TestEnvironment::substitute("{{ test_tmpdir }}/ocsp_test_data/" + filename); + } + + std::vector readFile(std::string filename) { + auto str = TestEnvironment::readFileToStringForTest(fullPath(filename)); + return {str.begin(), str.end()}; + } + + void setup(std::string response_filename) { + auto der_response = readFile(response_filename); + response_ = std::make_unique(der_response, time_system_); + EXPECT_EQ(response_->rawBytes(), der_response); + } + + void expectSuccessful() { + EXPECT_EQ(OcspResponseStatus::Successful, response_->getResponseStatus()); + } + + void expectCertificateMatches(std::string cert_filename) { + auto cert_ = readCertFromFile(fullPath(cert_filename)); + EXPECT_TRUE(response_->matchesCertificate(*cert_)); + } + +protected: + Event::SimulatedTimeSystem time_system_; + OcspResponseWrapperPtr response_; +}; + +TEST_F(OcspFullResponseParsingTest, GoodCertTest) { + setup("good_ocsp_resp.der"); + expectSuccessful(); + expectCertificateMatches("good_cert.pem"); + + auto cert = readCertFromFile(fullPath("revoked_cert.pem")); + EXPECT_FALSE(response_->matchesCertificate(*cert)); + + // Contains nextUpdate that is in the future + EXPECT_FALSE(response_->isExpired()); +} + +TEST_F(OcspFullResponseParsingTest, RevokedCertTest) { + setup("revoked_ocsp_resp.der"); + expectSuccessful(); + expectCertificateMatches("revoked_cert.pem"); + EXPECT_TRUE(response_->isExpired()); +} + +TEST_F(OcspFullResponseParsingTest, UnknownCertTest) { + setup("unknown_ocsp_resp.der"); + expectSuccessful(); + expectCertificateMatches("good_cert.pem"); + EXPECT_TRUE(response_->isExpired()); +} + +TEST_F(OcspFullResponseParsingTest, ExpiredResponseTest) { + auto next_week = time_system_.systemTime() + std::chrono::hours(8 * 24); + time_system_.setSystemTime(next_week); + setup("good_ocsp_resp.der"); + // nextUpdate is present but in the past + EXPECT_TRUE(response_->isExpired()); +} + +TEST_F(OcspFullResponseParsingTest, ThisUpdateAfterNowTest) { + auto past_time = TestUtility::parseTime("2000 01 01", "%Y %m %d"); + time_system_.setSystemTime(absl::ToChronoTime(past_time)); + EXPECT_LOG_CONTAINS("warning", "OCSP Response thisUpdate field is set in the future", + setup("good_ocsp_resp.der")); +} + +TEST_F(OcspFullResponseParsingTest, ResponderIdKeyHashTest) { + setup("responder_key_hash_ocsp_resp.der"); + expectSuccessful(); + expectCertificateMatches("good_cert.pem"); + EXPECT_TRUE(response_->isExpired()); +} + +TEST_F(OcspFullResponseParsingTest, MultiCertResponseTest) { + auto resp_bytes = readFile("multiple_cert_ocsp_resp.der"); + EXPECT_THROW_WITH_MESSAGE(OcspResponseWrapper response(resp_bytes, time_system_), EnvoyException, + "OCSP Response must be for one certificate only"); +} + +TEST_F(OcspFullResponseParsingTest, NoResponseBodyTest) { + std::vector data = { + // SEQUENCE + 0x30, 3, + // OcspResponseStatus - InternalError + 0xau, 1, 2, + // no response bytes + }; + EXPECT_THROW_WITH_MESSAGE(OcspResponseWrapper response(data, time_system_), EnvoyException, + "OCSP response has no body"); +} + +TEST_F(OcspFullResponseParsingTest, OnlyOneResponseInByteStringTest) { + auto resp_bytes = readFile("good_ocsp_resp.der"); + auto resp2_bytes = readFile("revoked_ocsp_resp.der"); + resp_bytes.insert(resp_bytes.end(), resp2_bytes.begin(), resp2_bytes.end()); + + EXPECT_THROW_WITH_MESSAGE(OcspResponseWrapper response_wrapper(resp_bytes, time_system_), + EnvoyException, "Data contained more than a single OCSP response"); +} + +TEST_F(OcspFullResponseParsingTest, ParseOcspResponseWrongTagTest) { + auto resp_bytes = readFile("good_ocsp_resp.der"); + // Change the SEQUENCE tag to an `OCTETSTRING` tag + resp_bytes[0] = 0x4u; + EXPECT_THROW_WITH_MESSAGE(OcspResponseWrapper response_wrapper(resp_bytes, time_system_), + EnvoyException, "OCSP Response is not a well-formed ASN.1 SEQUENCE"); +} + +class Asn1OcspUtilityTest : public testing::Test { +public: + void expectResponseStatus(uint8_t code, OcspResponseStatus expected) { + std::vector asn1_enum = {0xau, 1, code}; + CBS cbs; + CBS_init(&cbs, asn1_enum.data(), asn1_enum.size()); + + EXPECT_EQ(expected, Asn1OcspUtility::parseResponseStatus(cbs)); + } + + void expectThrowOnWrongTag(std::function parse) { + CBS cbs; + CBS_init(&cbs, asn1_true.data(), asn1_true.size()); + EXPECT_THROW(parse(cbs), EnvoyException); + } + + const std::vector asn1_true = {0x1u, 1, 0xff}; +}; + +TEST_F(Asn1OcspUtilityTest, ParseResponseStatusTest) { + expectResponseStatus(0, OcspResponseStatus::Successful); + expectResponseStatus(1, OcspResponseStatus::MalformedRequest); + expectResponseStatus(2, OcspResponseStatus::InternalError); + expectResponseStatus(3, OcspResponseStatus::TryLater); + expectResponseStatus(5, OcspResponseStatus::SigRequired); + expectResponseStatus(6, OcspResponseStatus::Unauthorized); +} + +TEST_F(Asn1OcspUtilityTest, ParseMethodWrongTagTest) { + expectThrowOnWrongTag(Asn1OcspUtility::parseResponseBytes); + expectThrowOnWrongTag(Asn1OcspUtility::parseBasicOcspResponse); + expectThrowOnWrongTag(Asn1OcspUtility::parseResponseData); + expectThrowOnWrongTag(Asn1OcspUtility::parseSingleResponse); + expectThrowOnWrongTag(Asn1OcspUtility::parseCertId); + expectThrowOnWrongTag(Asn1OcspUtility::parseResponseStatus); +} + +TEST_F(Asn1OcspUtilityTest, ParseResponseDataBadResponderIdVariantTest) { + std::vector data = { + // SEQUENCE + 0x30, + 6, + // version + 0, + 1, + 0, + // Invalid Responder ID tag 3 + 3, + 1, + 0, + }; + CBS cbs; + CBS_init(&cbs, data.data(), data.size()); + EXPECT_THROW_WITH_MESSAGE(Asn1OcspUtility::parseResponseData(cbs), EnvoyException, + "Unknown choice for Responder ID: 3"); +} + +TEST_F(Asn1OcspUtilityTest, ParseOcspResponseBytesMissingTest) { + std::vector data = { + // SEQUENCE + 0x30, 3, + // OcspResponseStatus - InternalError + 0xau, 1, 2, + // no response bytes + }; + CBS cbs; + CBS_init(&cbs, data.data(), data.size()); + auto response = Asn1OcspUtility::parseOcspResponse(cbs); + EXPECT_EQ(response->status_, OcspResponseStatus::InternalError); + EXPECT_TRUE(response->response_ == nullptr); +} + +TEST_F(Asn1OcspUtilityTest, ParseResponseStatusUnknownVariantTest) { + std::vector bad_enum_variant = {0xau, 1, 4}; + CBS cbs; + CBS_init(&cbs, bad_enum_variant.data(), bad_enum_variant.size()); + EXPECT_THROW_WITH_MESSAGE(Asn1OcspUtility::parseResponseStatus(cbs), EnvoyException, + "Unknown OCSP Response Status variant: 4"); +} + +TEST_F(Asn1OcspUtilityTest, ParseResponseBytesNoOctetStringTest) { + std::string oid_str = "1.1.1.1.1.1.1"; + bssl::ScopedCBB cbb; + CBB seq, oid, obj; + uint8_t* buf; + size_t buf_len; + + ASSERT_TRUE(CBB_init(cbb.get(), 0)); + ASSERT_TRUE(CBB_add_asn1(cbb.get(), &seq, CBS_ASN1_SEQUENCE)); + ASSERT_TRUE(CBB_add_asn1(&seq, &oid, CBS_ASN1_OBJECT)); + ASSERT_TRUE(CBB_add_asn1_oid_from_text(&oid, oid_str.c_str(), oid_str.size())); + // Empty sequence instead of `OCTETSTRING` with the response + ASSERT_TRUE(CBB_add_asn1(&seq, &obj, CBS_ASN1_SEQUENCE)); + ASSERT_TRUE(CBB_finish(cbb.get(), &buf, &buf_len)); + + CBS cbs; + CBS_init(&cbs, buf, buf_len); + bssl::UniquePtr scoped(buf); + + EXPECT_THROW_WITH_MESSAGE(Asn1OcspUtility::parseResponseBytes(cbs), EnvoyException, + "Expected ASN.1 OCTETSTRING for response"); +} + +TEST_F(Asn1OcspUtilityTest, ParseResponseBytesUnknownResponseTypeTest) { + std::string oid_str = "1.1.1.1.1.1.1"; + bssl::ScopedCBB cbb; + CBB seq, oid, obj; + uint8_t* buf; + size_t buf_len; + + ASSERT_TRUE(CBB_init(cbb.get(), 0)); + ASSERT_TRUE(CBB_add_asn1(cbb.get(), &seq, CBS_ASN1_SEQUENCE)); + ASSERT_TRUE(CBB_add_asn1(&seq, &oid, CBS_ASN1_OBJECT)); + ASSERT_TRUE(CBB_add_asn1_oid_from_text(&oid, oid_str.c_str(), oid_str.size())); + ASSERT_TRUE(CBB_add_asn1(&seq, &obj, CBS_ASN1_OCTETSTRING)); + ASSERT_TRUE(CBB_add_bytes(&obj, reinterpret_cast("\x1\x2\x3"), 3)); + ASSERT_TRUE(CBB_finish(cbb.get(), &buf, &buf_len)); + + CBS cbs; + CBS_init(&cbs, buf, buf_len); + bssl::UniquePtr scoped(buf); + + EXPECT_THROW_WITH_MESSAGE(Asn1OcspUtility::parseResponseBytes(cbs), EnvoyException, + "Unknown OCSP Response type with OID: 1.1.1.1.1.1.1"); +} + +} // namespace + +} // namespace Ocsp +} // namespace Tls +} // namespace TransportSockets +} // namespace Extensions +} // namespace Envoy diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index 40f0ea30ffce8..4ffedaa0d4677 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -420,6 +420,7 @@ bazel behaviour benchmarked bidi +bignum bitfield bitset bitwise @@ -430,6 +431,7 @@ bool boolean booleans bools +boringssl borks broadcasted buf @@ -441,6 +443,7 @@ bulkstrings bursty bytecode bytestream +bytestring cacheable cacheability callee