blob: 8a1e11b6fa5b30c945b5f4377d72559c9b1dc2d5 [file]
// Copyright 1995-2016 The OpenSSL Project Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <inttypes.h>
#include <string.h>
#include <algorithm>
#include <string_view>
#include <openssl/buf.h>
#include <openssl/err.h>
#include <openssl/mem.h>
#include <openssl/x509.h>
#include "../internal.h"
#include "../mem_internal.h"
#include "internal.h"
BSSL_NAMESPACE_BEGIN
// A ByDirEntry tracks state for a single directory, notably the starting suffix
// for CRL lookups.
class ByDirEntry {
public:
static constexpr bool kAllowUniquePtr = true;
ByDirEntry() = default;
static UniquePtr<ByDirEntry> Create(int dir_type, std::string_view dir) {
auto ret = MakeUnique<ByDirEntry>();
ret->dir_type_ = dir_type;
ret->dir_.reset(OPENSSL_strndup(dir.data(), dir.size()));
if (ret->dir_ == nullptr) {
return nullptr;
}
return ret;
}
int dir_type() const { return dir_type_; }
const char *dir() const { return dir_.get(); }
int GetCRLSuffix(uint32_t hash) const {
MutexReadLock lock(&lock_);
auto it = std::lower_bound(crl_suffixes_.begin(), crl_suffixes_.end(), hash);
if (it == crl_suffixes_.end() || it->hash != hash) {
return 0;
}
return it->suffix;
}
bool UpdateCRLSuffix(uint32_t hash, int suffix) {
MutexWriteLock lock(&lock_);
auto it = std::lower_bound(crl_suffixes_.begin(), crl_suffixes_.end(), hash);
if (it != crl_suffixes_.end() && it->hash == hash) {
it->suffix = std::max(suffix, it->suffix);
return true;
}
if (!crl_suffixes_.Push(CRLSuffix{hash, suffix})) {
return false;
}
std::sort(crl_suffixes_.begin(), crl_suffixes_.end());
return true;
}
private:
struct CRLSuffix {
uint32_t hash;
int suffix;
bool operator<(uint32_t h) const { return hash < h; }
bool operator<(const CRLSuffix &other) const { return hash < other.hash; }
};
UniquePtr<char> dir_;
int dir_type_ = 0;
mutable Mutex lock_;
// crl_suffixes_ is kept sorted.
// TODO(davidben): This should be a hash table. Insertions are O(N log N).
Vector<CRLSuffix> crl_suffixes_;
};
struct ByDir {
Vector<UniquePtr<ByDirEntry>> dirs;
};
static int dir_ctrl(X509_LOOKUP *ctx, int cmd, const char *argp, long argl,
char **ret);
static int new_dir(X509_LOOKUP *lu);
static void free_dir(X509_LOOKUP *lu);
static int add_cert_dir(ByDir *ctx, const char *dir, int type);
static int get_cert_by_subject(X509_LOOKUP *xl, int type, const X509_NAME *name,
X509_OBJECT *ret);
static const X509_LOOKUP_METHOD x509_dir_lookup = {
new_dir, // new
free_dir, // free
dir_ctrl, // ctrl
get_cert_by_subject, // get_by_subject
};
static int dir_ctrl(X509_LOOKUP *ctx, int cmd, const char *argp, long argl,
char **retp) {
ByDir *ld = reinterpret_cast<ByDir *>(ctx->method_data);
switch (cmd) {
case X509_L_ADD_DIR:
if (argl == X509_FILETYPE_DEFAULT) {
const char *dir = getenv(X509_get_default_cert_dir_env());
if (!add_cert_dir(ld, dir ? dir : X509_get_default_cert_dir(),
X509_FILETYPE_PEM)) {
OPENSSL_PUT_ERROR(X509, X509_R_LOADING_CERT_DIR);
return 0;
}
return 1;
}
return add_cert_dir(ld, argp, (int)argl);
}
return 0;
}
static int new_dir(X509_LOOKUP *lu) {
ByDir *a = New<ByDir>();
if (a == nullptr) {
return 0;
}
lu->method_data = a;
return 1;
}
static void free_dir(X509_LOOKUP *lu) {
Delete(reinterpret_cast<ByDir *>(lu->method_data));
}
#if defined(OPENSSL_WINDOWS)
#define DIR_HASH_SEPARATOR ';'
#else
#define DIR_HASH_SEPARATOR ':'
#endif
static int add_cert_dir(ByDir *ctx, const char *inp, int type) {
if (inp == nullptr || !*inp) {
OPENSSL_PUT_ERROR(X509, X509_R_INVALID_DIRECTORY);
return 0;
}
std::string_view rest = inp;
do {
// Split by `DIR_HASH_SEPARATOR`.
size_t sep = rest.find(DIR_HASH_SEPARATOR);
std::string_view dir;
if (sep == std::string_view::npos) {
dir = rest;
rest = std::string_view();
} else {
dir = rest.substr(0, sep);
rest = rest.substr(sep + 1);
}
if (dir.empty()) {
continue;
}
// Ignore duplicates.
if (std::any_of(ctx->dirs.begin(), ctx->dirs.end(),
[&](const auto &ent) { return ent->dir() == dir; })) {
continue;
}
auto ent = ByDirEntry::Create(type, dir);
if (ent == nullptr || !ctx->dirs.Push(std::move(ent))) {
return 0;
}
} while (!rest.empty());
return 1;
}
static int get_cert_by_subject(X509_LOOKUP *xl, int type, const X509_NAME *name,
X509_OBJECT *ret) {
if (name == nullptr) {
return 0;
}
// Set up an |X509_OBJECT| to compare against.
UniquePtr<X509> lookup_cert;
UniquePtr<X509_CRL> lookup_crl;
X509_OBJECT stmp;
const char *postfix = "";
stmp.type = type;
ByDir *ctx = reinterpret_cast<ByDir *>(xl->method_data);
if (type == X509_LU_X509) {
lookup_cert.reset(X509_new());
if (lookup_cert == nullptr ||
!X509_set_subject_name(lookup_cert.get(), name)) {
return 0;
}
stmp.data.x509 = lookup_cert.get();
postfix = "";
} else if (type == X509_LU_CRL) {
lookup_crl.reset(X509_CRL_new());
if (lookup_crl == nullptr ||
!X509_CRL_set_issuer_name(lookup_crl.get(), name)) {
return 0;
}
stmp.data.crl = lookup_crl.get();
postfix = "r";
} else {
OPENSSL_PUT_ERROR(X509, X509_R_WRONG_LOOKUP_TYPE);
return 0;
}
// Try both new and old hashes.
const uint32_t hashes[] = {X509_NAME_hash(name), X509_NAME_hash_old(name)};
for (uint32_t hash : hashes) {
for (UniquePtr<ByDirEntry> &ent : ctx->dirs) {
// If a CRL, start from the previously saved suffix. Updated CRLs are
// expected to be added until new filenames.
// TODO(crbug.com/42290566): Is this what we want?
int suffix = 0;
if (type == X509_LU_CRL) {
suffix = ent->GetCRLSuffix(hash);
}
// The directory format handles hash collections by incrementing a suffix
// on the file name. Load every suffix into the cache.
for (;;) {
char *path = nullptr;
if (OPENSSL_asprintf(&path, "%s/%08" PRIx32 ".%s%d", ent->dir(), hash,
postfix, suffix) == -1) {
OPENSSL_PUT_ERROR(X509, ERR_R_BUF_LIB);
return 0;
}
UniquePtr<char> free_path(path);
if (type == X509_LU_X509) {
if ((X509_load_cert_file(xl, path, ent->dir_type())) == 0) {
// Don't expose the lower level error, All of these boil down to "we
// could not find a CA".
ERR_clear_error();
break;
}
} else if (type == X509_LU_CRL) {
if ((X509_load_crl_file(xl, path, ent->dir_type())) == 0) {
// Don't expose the lower level error, All of these boil down to "we
// could not find a CRL".
ERR_clear_error();
break;
}
}
// The lack of a CA or CRL will be caught higher up.
suffix++;
}
// We have added it to the cache so now pull it out again.
auto *store_impl = FromOpaque(xl->store_ctx);
store_impl->objs_lock.LockWrite();
const X509_OBJECT *found = nullptr;
sk_X509_OBJECT_sort(store_impl->objs.get());
size_t idx;
if (sk_X509_OBJECT_find(store_impl->objs.get(), &idx, &stmp)) {
found = sk_X509_OBJECT_value(store_impl->objs.get(), idx);
}
store_impl->objs_lock.UnlockWrite();
// If a CRL, store the last suffix we saw, to skip already loaded files
// next time.
// TODO(crbug.com/42290566): Is this what we want?
if (type == X509_LU_CRL && !ent->UpdateCRLSuffix(hash, suffix)) {
return 0;
}
if (found != nullptr) {
// Clear any errors that might have been raised processing empty or
// malformed files.
ERR_clear_error();
// TODO(crbug.com/42290561): This should manage the reference counts
// correctly but does not.
ret->type = found->type;
OPENSSL_memcpy(&ret->data, &found->data, sizeof(ret->data));
return 1;
}
}
}
return 0;
}
BSSL_NAMESPACE_END
const X509_LOOKUP_METHOD *X509_LOOKUP_hash_dir() {
return &bssl::x509_dir_lookup;
}
int X509_LOOKUP_add_dir(X509_LOOKUP *lookup, const char *name, int type) {
return X509_LOOKUP_ctrl(lookup, X509_L_ADD_DIR, name, type, nullptr);
}