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

spiffe: add support for spiffe bundle format #36190

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE;
//
// Note that SPIFFE validator inherits and uses the following options from :ref:`CertificateValidationContext <envoy_v3_api_msg_extensions.transport_sockets.tls.v3.CertificateValidationContext>`.
//

// - :ref:`allow_expired_certificate <envoy_v3_api_field_extensions.transport_sockets.tls.v3.CertificateValidationContext.allow_expired_certificate>` to allow expired certificates.
// - :ref:`match_typed_subject_alt_names <envoy_v3_api_field_extensions.transport_sockets.tls.v3.CertificateValidationContext.match_typed_subject_alt_names>` to match **URI** SAN of certificates. Unlike the default validator, SPIFFE validator only matches **URI** SAN (which equals to SVID in SPIFFE terminology) and ignore other SAN types.
//

message SPIFFECertValidatorConfig {
message TrustDomain {
// Name of the trust domain, ``example.com``, ``foo.bar.gov`` for example.
Expand All @@ -57,4 +59,11 @@ message SPIFFECertValidatorConfig {

// This field specifies trust domains used for validating incoming X.509-SVID(s).
repeated TrustDomain trust_domains = 1 [(validate.rules).repeated = {min_items: 1}];
wbpcode marked this conversation as resolved.
Show resolved Hide resolved

// This field specifies all trust bundles as a single DataSource. If both
// trust_bundles and trust_domains are specified, trust_bundles will
// take precedence. Currently assumes file will be a SPIFFE Trust Bundle Map.
// If DataSource is a file, dynamic file watching will be enabled,
// and updates to the specified file will trigger a refresh of the trust_bundles.
config.core.v3.DataSource trust_bundles = 2;
}
7 changes: 7 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,13 @@ new_features:
Added an optional field :ref:`credential_provider
<envoy_v3_api_field_extensions.filters.http.aws_request_signing.v3.AwsRequestSigning.credential_provider>`
to the AWS request signing filter to explicitly specify a source for AWS credentials.
- area: spiffe
change: |
Added :ref:`trust_bundles
<envoy_v3_api_field_extensions.transport_sockets.tls.v3.SPIFFECertValidatorConfig.trust_bundles>`
to the SPIFFE certificate validator configuration. This field allows specifying a SPIFFE trust
bundle mapping as a DataSource. If both trust_bundles and trust_domains are specified,
trust_bundles takes precedence.
- area: tls
change: |
Added support for P-384 and P-521 curves for TLS server certificates.
Expand Down
2 changes: 0 additions & 2 deletions source/common/config/datasource.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ namespace Envoy {
namespace Config {
namespace DataSource {

namespace {
/**
* Read contents of the file.
* @param path file path.
Expand Down Expand Up @@ -48,7 +47,6 @@ absl::StatusOr<std::string> readFile(const std::string& path, Api::Api& api, boo

return file_content_or_error.value();
}
} // namespace

absl::StatusOr<std::string> read(const envoy::config::core::v3::DataSource& source,
bool allow_empty, Api::Api& api, uint64_t max_size) {
Expand Down
13 changes: 13 additions & 0 deletions source/common/config/datasource.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ using ProtoDataSource = envoy::config::core::v3::DataSource;
using ProtoWatchedDirectory = envoy::config::core::v3::WatchedDirectory;
using DataSourceProviderPtr = std::unique_ptr<DataSourceProvider>;

/**
* Read contents of the file.
* @param path file path.
* @param api reference to the Api.
* @param allow_empty return an empty string if the file is empty.
* @param max_size max size limit of file to read, default 0 means no limit, and if the file data
* would exceed the limit, it will return an error status.
* @return std::string with file contents. or an error status if the file does not exist or
* cannot be read.
*/
absl::StatusOr<std::string> readFile(const std::string& path, Api::Api& api, bool allow_empty,
uint64_t max_size = 0);

/**
* Read contents of the DataSource.
* @param source data source.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ envoy_cc_extension(
"//source/common/common:utility_lib",
"//source/common/config:datasource_lib",
"//source/common/config:utility_lib",
"//source/common/json:json_loader_lib",
"//source/common/stats:symbol_table_lib",
"//source/common/stats:utility_lib",
"//source/common/tls:stats_lib",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
#include "envoy/ssl/context_config.h"
#include "envoy/ssl/ssl_socket_extended_info.h"

#include "source/common/common/base64.h"
#include "source/common/common/utility.h"
#include "source/common/config/datasource.h"
#include "source/common/config/utility.h"
#include "source/common/json/json_loader.h"
#include "source/common/protobuf/message_validator_impl.h"
#include "source/common/stats/symbol_table.h"
#include "source/common/tls/cert_validator/factory.h"
Expand All @@ -30,10 +33,128 @@ namespace Tls {

using SPIFFEConfig = envoy::extensions::transport_sockets::tls::v3::SPIFFECertValidatorConfig;

std::shared_ptr<SpiffeData>
SPIFFEValidator::parseTrustBundles(const std::string& trust_bundle_mapping_str) {
briansonnenberg marked this conversation as resolved.
Show resolved Hide resolved
Json::ObjectSharedPtr parsed_json_bundle;

ENVOY_LOG(info, "Parsing trust_bundles");

auto json_parse_result = Envoy::Json::Factory::loadFromString(trust_bundle_mapping_str);
if (!json_parse_result.ok()) {
ENVOY_LOG(error, "Failed to parse SPIFFE bundle map JSON");
return nullptr;
}

parsed_json_bundle = json_parse_result.value();

std::shared_ptr<SpiffeData> spiffe_data = std::make_shared<SpiffeData>();

const auto trust_domains = parsed_json_bundle->getObject("trust_domains");

if (!trust_domains.ok() || *trust_domains == nullptr || (*trust_domains)->empty()) {
ENVOY_LOG(error, "No trust domains found in SPIFFE bundle map");
return nullptr;
}

bool success = true;
briansonnenberg marked this conversation as resolved.
Show resolved Hide resolved

auto status =
(*trust_domains)
->iterate([&spiffe_data, &success](const std::string& domain_name,
const Envoy::Json::Object& domain_object) -> bool {
// TODO: Duplicates are currently ignored and only the last value is used.
// This is because our json parser auto de-dupes keys in the dict and
// only include the last one in this iteration function.
spiffe_data->trust_bundle_stores[domain_name] = X509StorePtr(X509_STORE_new());

ENVOY_LOG(info, "Loading domain '{}' from SPIFFE bundle map", domain_name);

const auto keys = domain_object.getObjectArray("keys");

if (!keys.ok() || keys->empty()) {
ENVOY_LOG(error, "No keys found in SPIFFE bundle for domain '{}'", domain_name);
briansonnenberg marked this conversation as resolved.
Show resolved Hide resolved
return (success = false);
briansonnenberg marked this conversation as resolved.
Show resolved Hide resolved
}

ENVOY_LOG(info, "Found '{}' keys for domain '{}'", keys->size(), domain_name);

for (const auto& key : *keys) {
const auto use = key->getString("use");
// Currently only support x509, not jwt.
if (!use.ok() || *use != "x509-svid") {
ENVOY_LOG(error, "missing or invalid 'use' field found in cert for domain: '{}'",
domain_name);
return (success = false);
briansonnenberg marked this conversation as resolved.
Show resolved Hide resolved
}
const auto& certs = key->getStringArray("x5c");
if (!certs.ok() || (*certs).size() == 0) {
ENVOY_LOG(error, "missing 'x5c' field found in keys for domain: '{}'", domain_name);
return (success = false);
}
for (const auto& cert : *certs) {
std::string decoded_cert = Envoy::Base64::decode(cert);
if (decoded_cert.empty()) {
ENVOY_LOG(error, "Invalid or empty cert decoded in domain '{}'", domain_name);
return (success = false);
}

const unsigned char* cert_data =
reinterpret_cast<const unsigned char*>(decoded_cert.data());
bssl::UniquePtr<X509> x509(d2i_X509(nullptr, &cert_data, decoded_cert.size()));
if (!x509) {
ENVOY_LOG(error,
"Failed to create x509 object while loading certs in domain '{}'",
domain_name);
return (success = false);
}
if (X509_STORE_add_cert(spiffe_data->trust_bundle_stores[domain_name].get(),
x509.get()) != 1) {
ENVOY_LOG(error, "Failed to add x509 object while loading certs for domain '{}'",
domain_name);
return (success = false);
}
spiffe_data->ca_certs.push_back(std::move(x509));
}
}

return true;
});

if (!status.ok() || !success) {
return nullptr;
}

ENVOY_LOG(info, "Successfully loaded SPIFFE bundle map");
return spiffe_data;
}

void SPIFFEValidator::initializeCertificateRefresh(
Server::Configuration::CommonFactoryContext& context) {
file_watcher_ = context.mainThreadDispatcher().createFilesystemWatcher();
THROW_IF_NOT_OK(file_watcher_->addWatch(
trust_bundle_file_name_, Filesystem::Watcher::Events::Modified, [this](uint32_t) {
ENVOY_LOG(info, "Updating SPIFFE bundle map from file '{}'", trust_bundle_file_name_);

auto read_result =
Envoy::Config::DataSource::readFile(trust_bundle_file_name_, api_, false);
if (!read_result.ok()) {
return absl::OkStatus();
ENVOY_LOG(error, "Failed to open SPIFFE bundle map file '{}'", trust_bundle_file_name_);
}

if (auto new_trust_bundle = parseTrustBundles(*read_result)) {
updateSpiffeData(new_trust_bundle);
} else {
ENVOY_LOG(error, "Failed to load SPIFFE bundle map from '{}'", trust_bundle_file_name_);
}
return absl::OkStatus();
}));
}

SPIFFEValidator::SPIFFEValidator(const Envoy::Ssl::CertificateValidationContextConfig* config,
SslStats& stats,
Server::Configuration::CommonFactoryContext& context)
: stats_(stats), time_source_(context.timeSource()) {
: api_(config->api()), stats_(stats), time_source_(context.timeSource()) {
ASSERT(config != nullptr);
allow_expired_certificate_ = config->allowExpiredCertificate();

Expand All @@ -55,10 +176,36 @@ SPIFFEValidator::SPIFFEValidator(const Envoy::Ssl::CertificateValidationContextC
}
}

const auto size = message.trust_domains().size();
trust_bundle_stores_.reserve(size);
const auto n_trust_domains = message.trust_domains().size();

// If a trust bundle map is provided, use that...
if (message.has_trust_bundles()) {
std::string trust_bundles_str = THROW_OR_RETURN_VALUE(
Config::DataSource::read(message.trust_bundles(), false, config->api()), std::string);
spiffe_data_ = parseTrustBundles(trust_bundles_str);

if (!spiffe_data_) {
throw EnvoyException("Failed to load SPIFFE Bundle map");
}

if (message.trust_bundles().has_filename()) {
trust_bundle_file_name_ = message.trust_bundles().filename();
// Set up dynamic refresh with tls_ and file watcher
tls_ = ThreadLocal::TypedSlot<ThreadLocalSpiffeState>::makeUnique(context.threadLocal());
tls_->set([](Event::Dispatcher&) { return std::make_shared<ThreadLocalSpiffeState>(); });
updateSpiffeData(spiffe_data_);
initializeCertificateRefresh(context);
}

return;
}

// User configured "trust_domains", not "trust_bundles"
spiffe_data_ = std::make_shared<SpiffeData>();
spiffe_data_->trust_bundle_stores.reserve(n_trust_domains);
for (auto& domain : message.trust_domains()) {
if (trust_bundle_stores_.find(domain.name()) != trust_bundle_stores_.end()) {
if (spiffe_data_->trust_bundle_stores.find(domain.name()) !=
spiffe_data_->trust_bundle_stores.end()) {
throw EnvoyException(absl::StrCat(
"Multiple trust bundles are given for one trust domain for ", domain.name()));
}
Expand All @@ -80,7 +227,7 @@ SPIFFEValidator::SPIFFEValidator(const Envoy::Ssl::CertificateValidationContextC
for (const X509_INFO* item : list.get()) {
if (item->x509) {
X509_STORE_add_cert(store.get(), item->x509);
ca_certs_.push_back(bssl::UniquePtr<X509>(item->x509));
spiffe_data_->ca_certs.push_back(bssl::UniquePtr<X509>(item->x509));
X509_up_ref(item->x509);
if (!ca_loaded) {
// TODO: With the current interface, we cannot return the multiple
Expand All @@ -102,7 +249,7 @@ SPIFFEValidator::SPIFFEValidator(const Envoy::Ssl::CertificateValidationContextC
if (has_crl) {
X509_STORE_set_flags(store.get(), X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL);
}
trust_bundle_stores_[domain.name()] = std::move(store);
spiffe_data_->trust_bundle_stores[domain.name()] = std::move(store);
}
}

Expand All @@ -112,7 +259,8 @@ absl::Status SPIFFEValidator::addClientValidationContext(SSL_CTX* ctx, bool) {
bssl::UniquePtr<STACK_OF(X509_NAME)> list(
sk_X509_NAME_new([](auto* a, auto* b) -> int { return X509_NAME_cmp(*a, *b); }));

for (auto& ca : ca_certs_) {
auto spiffe_data = getSpiffeData();
for (auto& ca : spiffe_data->ca_certs) {
X509_NAME* name = X509_get_subject_name(ca.get());

// Check for duplicates.
Expand All @@ -133,7 +281,8 @@ void SPIFFEValidator::updateDigestForSessionId(bssl::ScopedEVP_MD_CTX& md,
uint8_t hash_buffer[EVP_MAX_MD_SIZE],
unsigned hash_length) {
int rc;
for (auto& ca : ca_certs_) {
auto spiffe_data = getSpiffeData();
for (auto& ca : spiffe_data->ca_certs) {
rc = X509_digest(ca.get(), EVP_sha256(), hash_buffer, &hash_length);
RELEASE_ASSERT(rc == 1, Utility::getLastCryptoError().value_or(""));
RELEASE_ASSERT(hash_length == SHA256_DIGEST_LENGTH,
Expand Down Expand Up @@ -238,12 +387,14 @@ X509_STORE* SPIFFEValidator::getTrustBundleStore(X509* leaf_cert) {
return nullptr;
}

auto target_store = trust_bundle_stores_.find(trust_domain);
return target_store != trust_bundle_stores_.end() ? target_store->second.get() : nullptr;
auto spiffe_data = getSpiffeData();
auto target_store = spiffe_data->trust_bundle_stores.find(trust_domain);
return target_store != spiffe_data->trust_bundle_stores.end() ? target_store->second.get()
: nullptr;
}

bool SPIFFEValidator::certificatePrecheck(X509* leaf_cert) {
// Check basic constrains and key usage.
// Check basic constraints and key usage.
// https://github.com/spiffe/spiffe/blob/master/standards/X509-SVID.md#52-leaf-validation
const auto ext = X509_get_extension_flags(leaf_cert);
if (ext & EXFLAG_CA) {
Expand Down Expand Up @@ -287,11 +438,12 @@ std::string SPIFFEValidator::extractTrustDomain(const std::string& san) {
}

absl::optional<uint32_t> SPIFFEValidator::daysUntilFirstCertExpires() const {
if (ca_certs_.empty()) {
auto spiffe_data = getSpiffeData();
if (spiffe_data->ca_certs.empty()) {
return absl::make_optional(std::numeric_limits<uint32_t>::max());
}
absl::optional<uint32_t> ret = absl::make_optional(std::numeric_limits<uint32_t>::max());
for (auto& cert : ca_certs_) {
for (auto& cert : spiffe_data->ca_certs) {
const absl::optional<uint32_t> tmp = Utility::getDaysUntilExpiration(cert.get(), time_source_);
if (!tmp.has_value()) {
return absl::nullopt;
Expand All @@ -303,12 +455,13 @@ absl::optional<uint32_t> SPIFFEValidator::daysUntilFirstCertExpires() const {
}

Envoy::Ssl::CertificateDetailsPtr SPIFFEValidator::getCaCertInformation() const {
if (ca_certs_.empty()) {
auto spiffe_data = getSpiffeData();
if (spiffe_data->ca_certs.empty()) {
return nullptr;
}
// TODO(mathetake): With the current interface, we cannot pass the multiple cert information.
// So temporarily we return the first CA's info here.
return Utility::certificateDetails(ca_certs_[0].get(), getCaFileName(), time_source_);
return Utility::certificateDetails(spiffe_data->ca_certs[0].get(), getCaFileName(), time_source_);
};

class SPIFFEValidatorFactory : public CertValidatorFactory {
Expand Down
Loading
Loading