mirror of
https://github.com/mirkobrombin/raven.git
synced 2026-11-03 21:46:32 +00:00
591 lines
25 KiB
C++
591 lines
25 KiB
C++
#include "authenticode.h"
|
|
|
|
#ifdef RAVEN_OPENSSL
|
|
#include <openssl/cms.h>
|
|
#include <openssl/evp.h>
|
|
#include <openssl/pkcs7.h>
|
|
#include <openssl/ts.h>
|
|
#include <openssl/x509_vfy.h>
|
|
#include <openssl/x509v3.h>
|
|
#endif
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string>
|
|
|
|
namespace raven {
|
|
|
|
#ifdef RAVEN_OPENSSL
|
|
|
|
namespace {
|
|
|
|
constexpr uint32_t trust_no_signature = 0x800b0100, trust_unknown_subject = 0x800b0003, trust_bad_digest = 0x80096010,
|
|
trust_bad_signature = 0x80096004, certificate_expired = 0x800b0101, certificate_untrusted_root = 0x800b0109,
|
|
certificate_chaining = 0x800b010a;
|
|
|
|
uint32_t le32(std::span<const uint8_t> bytes, size_t at)
|
|
{
|
|
uint32_t value = 0;
|
|
if (at + 4 <= bytes.size())
|
|
std::memcpy(&value, bytes.data() + at, 4);
|
|
return value;
|
|
}
|
|
|
|
uint16_t le16(std::span<const uint8_t> bytes, size_t at)
|
|
{
|
|
uint16_t value = 0;
|
|
if (at + 2 <= bytes.size())
|
|
std::memcpy(&value, bytes.data() + at, 2);
|
|
return value;
|
|
}
|
|
|
|
// A signed file: the PKCS #7 signature it carries, and how its content is hashed for the digest the signature names.
|
|
struct Signed {
|
|
std::vector<uint8_t> signature;
|
|
std::function<void(EVP_MD_CTX *)> hash;
|
|
};
|
|
|
|
void update(EVP_MD_CTX *context, std::span<const uint8_t> bytes, size_t from, size_t to)
|
|
{
|
|
if (from < to && to <= bytes.size())
|
|
EVP_DigestUpdate(context, bytes.data() + from, to - from);
|
|
}
|
|
|
|
// A program or library: the WIN_CERTIFICATE the security directory names; the digest covers the file but the checksum, the security
|
|
// directory entry and the certificate table.
|
|
std::optional<Signed> portable_executable(std::span<const uint8_t> file)
|
|
{
|
|
const uint32_t header = le32(file, 0x3c);
|
|
if (header + 24 + 96 > file.size() || le32(file, header) != 0x4550)
|
|
return std::nullopt;
|
|
const size_t optional = header + 24, checksum = optional + 64;
|
|
const size_t directories = optional + (le16(file, optional) == 0x20b ? 112 : 96), security = directories + 4 * 8;
|
|
const uint32_t table = le32(file, security), size = le32(file, security + 4);
|
|
if (!table || size < 8 || uint64_t(table) + size > file.size())
|
|
return Signed{};
|
|
const uint32_t length = le32(file, table);
|
|
if (le16(file, table + 6) != 2 || length < 8 || length > size)
|
|
return Signed{};
|
|
return Signed{std::vector<uint8_t>(file.begin() + table + 8, file.begin() + table + length), [file, checksum, security, table](EVP_MD_CTX *context) {
|
|
update(context, file, 0, checksum);
|
|
update(context, file, checksum + 4, security);
|
|
update(context, file, security + 8, table);
|
|
}};
|
|
}
|
|
|
|
// A cabinet: the per-cabinet reserve says where the signature follows the cabinet; the digest covers the header fields that do not
|
|
// change when the cabinet is signed, the folder entries and everything from the file entries to the signature.
|
|
std::optional<Signed> cabinet(std::span<const uint8_t> file)
|
|
{
|
|
const uint16_t flags = le16(file, 30);
|
|
if (!(flags & 4) || le16(file, 36) < 20)
|
|
return Signed{};
|
|
const uint32_t at = le32(file, 44), size = le32(file, 48);
|
|
if (!at || uint64_t(at) + size > file.size())
|
|
return Signed{};
|
|
return Signed{std::vector<uint8_t>(file.begin() + at, file.begin() + at + size), [file, flags, at](EVP_MD_CTX *context) {
|
|
// The signature, the size and the offset of the file entries, the version, the counts, the flags, the set, and the end of
|
|
// the reserve; not the reserved words, the cabinet's place in its set or where the signature is.
|
|
static constexpr std::pair<size_t, size_t> fields[] = {{0, 4}, {8, 16}, {16, 20}, {20, 26}, {26, 30}, {30, 32}, {32, 34}, {56, 60}};
|
|
for (const auto &[from, to] : fields)
|
|
update(context, file, from, to);
|
|
size_t cursor = 36 + 4 + le16(file, 36);
|
|
// The names of the previous and next cabinets of a set, with their terminators.
|
|
for (const uint16_t flag : {1, 2})
|
|
if (flags & flag)
|
|
for (int text = 0; text < 2; ++text) {
|
|
const size_t start = cursor;
|
|
while (cursor < file.size() && file[cursor])
|
|
++cursor;
|
|
update(context, file, start, ++cursor);
|
|
}
|
|
const size_t folder_size = 8 + file[38];
|
|
for (uint16_t folder = 0; folder < le16(file, 26); ++folder)
|
|
update(context, file, cursor + folder * folder_size, cursor + folder * folder_size + 8);
|
|
update(context, file, le32(file, 16), at);
|
|
}};
|
|
}
|
|
|
|
// The parts of a compound file, the container of installer databases: its directory and the content of each stream.
|
|
class CompoundFile {
|
|
public:
|
|
struct Entry {
|
|
std::vector<uint8_t> name;
|
|
uint8_t type = 0;
|
|
uint32_t left = 0, right = 0, child = 0, start = 0, size = 0;
|
|
std::array<uint8_t, 16> clsid{};
|
|
// The entry's state bits, its creation and modification times and the low half of its size, as stored.
|
|
std::array<uint8_t, 4> state{}, size_low{};
|
|
std::array<uint8_t, 16> times{};
|
|
};
|
|
|
|
explicit CompoundFile(std::span<const uint8_t> file) : file(file)
|
|
{
|
|
sector = size_t(1) << le16(file, 30);
|
|
mini_sector = size_t(1) << le16(file, 32);
|
|
cutoff = le32(file, 56);
|
|
std::vector<uint32_t> fat_sectors;
|
|
for (size_t index = 0; index < 109; ++index)
|
|
fat_sectors.push_back(le32(file, 76 + index * 4));
|
|
for (uint32_t next = le32(file, 68), count = le32(file, 72); count && next < 0xfffffffa && fat_sectors.size() < 1 << 20; --count) {
|
|
for (size_t index = 0; index + 1 < sector / 4; ++index)
|
|
fat_sectors.push_back(le32(file, offset(next) + index * 4));
|
|
next = le32(file, offset(next) + sector - 4);
|
|
}
|
|
fat_sectors.resize(std::min<size_t>(fat_sectors.size(), le32(file, 44)));
|
|
for (const auto at : fat_sectors)
|
|
for (size_t index = 0; index < sector / 4; ++index)
|
|
fat.push_back(le32(file, offset(at) + index * 4));
|
|
const auto directory = read(le32(file, 48));
|
|
for (size_t at = 0; at + 128 <= directory.size(); at += 128) {
|
|
Entry entry;
|
|
const size_t name_size = std::min<size_t>(le16(directory, at + 64), 64);
|
|
entry.name.assign(directory.begin() + at, directory.begin() + at + name_size);
|
|
entry.type = directory[at + 66];
|
|
entry.left = le32(directory, at + 68);
|
|
entry.right = le32(directory, at + 72);
|
|
entry.child = le32(directory, at + 76);
|
|
std::memcpy(entry.clsid.data(), directory.data() + at + 80, 16);
|
|
std::memcpy(entry.state.data(), directory.data() + at + 96, 4);
|
|
std::memcpy(entry.times.data(), directory.data() + at + 100, 16);
|
|
std::memcpy(entry.size_low.data(), directory.data() + at + 120, 4);
|
|
entry.start = le32(directory, at + 116);
|
|
entry.size = le32(directory, at + 120);
|
|
entries.push_back(std::move(entry));
|
|
}
|
|
if (!entries.empty()) {
|
|
mini_stream = read(entries[0].start, entries[0].size);
|
|
const auto table = read(le32(file, 60));
|
|
for (size_t at = 0; at + 4 <= table.size(); at += 4)
|
|
mini_fat.push_back(le32(table, at));
|
|
}
|
|
}
|
|
|
|
std::vector<uint8_t> stream(const Entry &entry) const
|
|
{
|
|
if (entry.size >= cutoff)
|
|
return read(entry.start, entry.size);
|
|
std::vector<uint8_t> bytes;
|
|
for (uint32_t at = entry.start; at < 0xfffffffa && at < mini_fat.size() && bytes.size() < entry.size; at = mini_fat[at])
|
|
if ((at + 1) * mini_sector <= mini_stream.size())
|
|
bytes.insert(bytes.end(), mini_stream.begin() + at * mini_sector, mini_stream.begin() + (at + 1) * mini_sector);
|
|
bytes.resize(std::min<size_t>(bytes.size(), entry.size));
|
|
return bytes;
|
|
}
|
|
|
|
// A storage's entries, from the tree its child entry roots.
|
|
std::vector<const Entry *> children(const Entry &storage) const
|
|
{
|
|
std::vector<const Entry *> found;
|
|
std::vector<uint32_t> pending{storage.child};
|
|
while (!pending.empty() && found.size() < entries.size()) {
|
|
const uint32_t at = pending.back();
|
|
pending.pop_back();
|
|
if (at >= entries.size())
|
|
continue;
|
|
found.push_back(&entries[at]);
|
|
pending.push_back(entries[at].left);
|
|
pending.push_back(entries[at].right);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
std::vector<Entry> entries;
|
|
|
|
private:
|
|
size_t offset(uint32_t at) const { return (size_t(at) + 1) * sector; }
|
|
std::vector<uint8_t> read(uint32_t at, size_t limit = SIZE_MAX) const
|
|
{
|
|
std::vector<uint8_t> bytes;
|
|
for (size_t steps = 0; at < 0xfffffffa && at < fat.size() && bytes.size() < limit && steps <= fat.size(); at = fat[at], ++steps)
|
|
if (offset(at) + sector <= file.size())
|
|
bytes.insert(bytes.end(), file.begin() + offset(at), file.begin() + offset(at) + sector);
|
|
if (limit != SIZE_MAX)
|
|
bytes.resize(std::min(bytes.size(), limit));
|
|
return bytes;
|
|
}
|
|
|
|
std::span<const uint8_t> file;
|
|
size_t sector = 512, mini_sector = 64, cutoff = 4096;
|
|
std::vector<uint32_t> fat, mini_fat;
|
|
std::vector<uint8_t> mini_stream;
|
|
};
|
|
|
|
// A directory entry's name as the compound file stores it: UTF-16 with its terminator.
|
|
std::vector<uint8_t> entry_name(std::u16string_view text)
|
|
{
|
|
std::vector<uint8_t> bytes;
|
|
for (const auto character : text) {
|
|
bytes.push_back(uint8_t(character));
|
|
bytes.push_back(uint8_t(character >> 8));
|
|
}
|
|
bytes.insert(bytes.end(), {0, 0});
|
|
return bytes;
|
|
}
|
|
|
|
// An installer database: the signature is the DigitalSignature stream; the digest covers every other stream, storage by storage in the
|
|
// order of their names as UTF-16 bytes, each storage followed by its class identifier. A database that also carries
|
|
// MsiDigitalSignatureEx has its entries' metadata hashed first, and that hash, which the stream holds, begins the digest.
|
|
std::optional<Signed> installer_database(std::span<const uint8_t> file)
|
|
{
|
|
auto compound = std::make_shared<CompoundFile>(file);
|
|
if (compound->entries.empty())
|
|
return std::nullopt;
|
|
const auto signature_name = entry_name(u"\x05" u"DigitalSignature"), extended_name = entry_name(u"\x05" u"MsiDigitalSignatureEx");
|
|
std::vector<uint8_t> signature, metadata;
|
|
bool extended = false;
|
|
for (const auto *entry : compound->children(compound->entries[0]))
|
|
if (entry->type == 2 && entry->name == signature_name)
|
|
signature = compound->stream(*entry);
|
|
else if (entry->type == 2 && entry->name == extended_name) {
|
|
metadata = compound->stream(*entry);
|
|
extended = true;
|
|
}
|
|
if (signature.empty())
|
|
return Signed{};
|
|
const auto sorted = [compound](const CompoundFile::Entry &storage) {
|
|
auto children = compound->children(storage);
|
|
std::sort(children.begin(), children.end(), [](const auto *left, const auto *right) {
|
|
const size_t common = std::min(left->name.size(), right->name.size());
|
|
const int order = std::memcmp(left->name.data(), right->name.data(), common);
|
|
return order ? order < 0 : left->name.size() < right->name.size();
|
|
});
|
|
return children;
|
|
};
|
|
const auto skipped = [signature_name, extended_name](const CompoundFile::Entry &entry) {
|
|
return entry.type == 2 && (entry.name == signature_name || entry.name == extended_name);
|
|
};
|
|
return Signed{std::move(signature), [compound, sorted, skipped, metadata, extended](EVP_MD_CTX *context) {
|
|
if (extended) {
|
|
// The metadata hash: every entry's name without its terminator (the root has none), the class identifier of a storage or
|
|
// the low half of a stream's size, the state bits, and the times of all but the root.
|
|
std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)> prehash(EVP_MD_CTX_new(), EVP_MD_CTX_free);
|
|
EVP_DigestInit_ex(prehash.get(), EVP_MD_CTX_get0_md(context), nullptr);
|
|
const auto describe = [&](const CompoundFile::Entry &entry) {
|
|
const bool root = entry.type == 5;
|
|
if (!root && entry.name.size() >= 2)
|
|
EVP_DigestUpdate(prehash.get(), entry.name.data(), entry.name.size() - 2);
|
|
if (entry.type == 2)
|
|
EVP_DigestUpdate(prehash.get(), entry.size_low.data(), entry.size_low.size());
|
|
else
|
|
EVP_DigestUpdate(prehash.get(), entry.clsid.data(), entry.clsid.size());
|
|
EVP_DigestUpdate(prehash.get(), entry.state.data(), entry.state.size());
|
|
if (!root)
|
|
EVP_DigestUpdate(prehash.get(), entry.times.data(), entry.times.size());
|
|
};
|
|
const std::function<void(const CompoundFile::Entry &, int)> walk = [&](const CompoundFile::Entry &storage, int depth) {
|
|
describe(storage);
|
|
for (const auto *child : sorted(storage)) {
|
|
if (skipped(*child))
|
|
continue;
|
|
if (child->type == 2)
|
|
describe(*child);
|
|
else if (child->type == 1 && depth < 32)
|
|
walk(*child, depth + 1);
|
|
}
|
|
};
|
|
walk(compound->entries[0], 0);
|
|
unsigned char computed[EVP_MAX_MD_SIZE];
|
|
unsigned int size = 0;
|
|
EVP_DigestFinal_ex(prehash.get(), computed, &size);
|
|
// Metadata that no longer matches its hash spoils the digest, as the signature then covers other content.
|
|
if (metadata.size() != size || std::memcmp(metadata.data(), computed, size) != 0)
|
|
EVP_DigestUpdate(context, computed, size);
|
|
EVP_DigestUpdate(context, metadata.data(), metadata.size());
|
|
}
|
|
const std::function<void(const CompoundFile::Entry &, int)> storage = [&](const CompoundFile::Entry &entry, int depth) {
|
|
for (const auto *child : sorted(entry)) {
|
|
if (child->type == 2 && !skipped(*child)) {
|
|
const auto bytes = compound->stream(*child);
|
|
EVP_DigestUpdate(context, bytes.data(), bytes.size());
|
|
} else if (child->type == 1 && depth < 32) {
|
|
storage(*child, depth + 1);
|
|
}
|
|
}
|
|
EVP_DigestUpdate(context, entry.clsid.data(), entry.clsid.size());
|
|
};
|
|
storage(compound->entries[0], 0);
|
|
}};
|
|
}
|
|
|
|
// A DER length after its tag at `at`: the header size and the content size.
|
|
std::optional<std::pair<size_t, size_t>> der_header(std::span<const uint8_t> bytes, size_t at)
|
|
{
|
|
if (at + 2 > bytes.size())
|
|
return std::nullopt;
|
|
const uint8_t first = bytes[at + 1];
|
|
if (first < 0x80)
|
|
return std::pair{size_t(2), size_t(first)};
|
|
const size_t count = first & 0x7f;
|
|
if (!count || count > 4 || at + 2 + count > bytes.size())
|
|
return std::nullopt;
|
|
size_t length = 0;
|
|
for (size_t index = 0; index < count; ++index)
|
|
length = length << 8 | bytes[at + 2 + index];
|
|
return std::pair{2 + count, length};
|
|
}
|
|
|
|
// The time a timestamp countersignature gives the signature, as seconds since 1970.
|
|
std::optional<time_t> stamped_time(PKCS7_SIGNER_INFO *signer)
|
|
{
|
|
const auto to_seconds = [](const ASN1_TIME *time) -> std::optional<time_t> {
|
|
std::tm parts{};
|
|
if (!time || !ASN1_TIME_to_tm(time, &parts))
|
|
return std::nullopt;
|
|
return timegm(&parts);
|
|
};
|
|
if (ASN1_TYPE *counter = PKCS7_get_attribute(signer, NID_pkcs9_countersignature); counter && counter->type == V_ASN1_SEQUENCE) {
|
|
const unsigned char *at = counter->value.sequence->data;
|
|
std::unique_ptr<PKCS7_SIGNER_INFO, decltype(&PKCS7_SIGNER_INFO_free)> info(
|
|
d2i_PKCS7_SIGNER_INFO(nullptr, &at, counter->value.sequence->length), PKCS7_SIGNER_INFO_free);
|
|
if (info)
|
|
if (ASN1_TYPE *signed_at = PKCS7_get_signed_attribute(info.get(), NID_pkcs9_signingTime); signed_at && signed_at->type == V_ASN1_UTCTIME)
|
|
return to_seconds(signed_at->value.utctime);
|
|
}
|
|
// An RFC 3161 timestamp token, which newer signatures carry under Microsoft's attribute; its identifier is compared as text, since the
|
|
// library may not know it.
|
|
const STACK_OF(X509_ATTRIBUTE) *unsigned_attributes = PKCS7_get_attributes(signer);
|
|
for (int index = 0; index < sk_X509_ATTRIBUTE_num(unsigned_attributes); ++index) {
|
|
X509_ATTRIBUTE *attribute = sk_X509_ATTRIBUTE_value(unsigned_attributes, index);
|
|
char name[64] = {};
|
|
OBJ_obj2txt(name, sizeof(name), X509_ATTRIBUTE_get0_object(attribute), 1);
|
|
ASN1_TYPE *token = X509_ATTRIBUTE_get0_type(attribute, 0);
|
|
if (std::strcmp(name, "1.3.6.1.4.1.311.3.3.1") != 0 || !token || token->type != V_ASN1_SEQUENCE)
|
|
continue;
|
|
// The token is CMS signed data, which the PKCS #7 parser does not accept in every form; its content is the TSTInfo.
|
|
const unsigned char *at = token->value.sequence->data;
|
|
std::unique_ptr<CMS_ContentInfo, decltype(&CMS_ContentInfo_free)> signed_token(
|
|
d2i_CMS_ContentInfo(nullptr, &at, token->value.sequence->length), CMS_ContentInfo_free);
|
|
ASN1_OCTET_STRING **content = signed_token ? CMS_get0_content(signed_token.get()) : nullptr;
|
|
if (content && *content) {
|
|
const unsigned char *info_at = (*content)->data;
|
|
std::unique_ptr<TS_TST_INFO, decltype(&TS_TST_INFO_free)> info(d2i_TS_TST_INFO(nullptr, &info_at, (*content)->length), TS_TST_INFO_free);
|
|
if (info)
|
|
return to_seconds(TS_TST_INFO_get_time(info.get()));
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::vector<uint8_t> der_of(X509 *certificate)
|
|
{
|
|
const int length = i2d_X509(certificate, nullptr);
|
|
std::vector<uint8_t> bytes(length > 0 ? size_t(length) : 0);
|
|
unsigned char *at = bytes.data();
|
|
if (length > 0)
|
|
i2d_X509(certificate, &at);
|
|
return bytes;
|
|
}
|
|
|
|
}
|
|
|
|
X509_STORE *trust_anchors()
|
|
{
|
|
static X509_STORE *store = [] {
|
|
auto *created = X509_STORE_new();
|
|
if (!created)
|
|
return created;
|
|
for (const char *path : {"/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt", "/etc/ssl/cert.pem"})
|
|
if (X509_STORE_load_file(created, path))
|
|
break;
|
|
std::error_code error;
|
|
const auto roots = std::filesystem::read_symlink("/proc/self/exe", error).parent_path() / "roots";
|
|
for (const auto &entry : std::filesystem::directory_iterator(roots, error)) {
|
|
std::ifstream input(entry.path(), std::ios::binary);
|
|
const std::string bytes((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
|
|
const auto *at = reinterpret_cast<const unsigned char *>(bytes.data());
|
|
if (X509 *certificate = d2i_X509(nullptr, &at, long(bytes.size()))) {
|
|
X509_STORE_add_cert(created, certificate);
|
|
X509_free(certificate);
|
|
}
|
|
}
|
|
return created;
|
|
}();
|
|
return store;
|
|
}
|
|
|
|
namespace {
|
|
|
|
std::optional<Signed> signed_content(std::span<const uint8_t> file)
|
|
{
|
|
if (file.size() >= 0x40 && file[0] == 'M' && file[1] == 'Z')
|
|
return portable_executable(file);
|
|
if (file.size() >= 60 && std::memcmp(file.data(), "MSCF", 4) == 0)
|
|
return cabinet(file);
|
|
if (file.size() >= 512 && le32(file, 0) == 0xe011cfd0 && le32(file, 4) == 0xe11ab1a1)
|
|
return installer_database(file);
|
|
return std::nullopt;
|
|
}
|
|
|
|
}
|
|
|
|
std::vector<uint8_t> embedded_signature(std::span<const uint8_t> file)
|
|
{
|
|
auto found = signed_content(file);
|
|
return found ? std::move(found->signature) : std::vector<uint8_t>();
|
|
}
|
|
|
|
std::vector<uint8_t> subject_digest(std::span<const uint8_t> file, bool sha256)
|
|
{
|
|
std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)> context(EVP_MD_CTX_new(), EVP_MD_CTX_free);
|
|
if (!context || !EVP_DigestInit_ex(context.get(), sha256 ? EVP_sha256() : EVP_sha1(), nullptr))
|
|
return {};
|
|
// A program or library is hashed as its signature would be, without the checksum, the security directory entry and the certificate
|
|
// table; any other file whole.
|
|
const uint32_t header = file.size() >= 0x40 && file[0] == 'M' && file[1] == 'Z' ? le32(file, 0x3c) : 0;
|
|
if (header && uint64_t(header) + 24 + 96 <= file.size() && le32(file, header) == 0x4550) {
|
|
const size_t optional = header + 24, checksum = optional + 64;
|
|
const size_t security = optional + (le16(file, optional) == 0x20b ? 112 : 96) + 4 * 8;
|
|
const uint32_t table = le32(file, security), size = le32(file, security + 4);
|
|
const size_t end = table && size && uint64_t(table) + size <= file.size() ? table : file.size();
|
|
update(context.get(), file, 0, checksum);
|
|
update(context.get(), file, checksum + 4, security);
|
|
update(context.get(), file, security + 8, end);
|
|
} else
|
|
update(context.get(), file, 0, file.size());
|
|
std::vector<uint8_t> digest(EVP_MAX_MD_SIZE);
|
|
unsigned int length = 0;
|
|
if (!EVP_DigestFinal_ex(context.get(), digest.data(), &length))
|
|
return {};
|
|
digest.resize(length);
|
|
return digest;
|
|
}
|
|
|
|
AuthenticodeResult verify_authenticode(std::span<const uint8_t> file)
|
|
{
|
|
AuthenticodeResult result;
|
|
std::optional<Signed> found = signed_content(file);
|
|
if (!found) {
|
|
result.status = trust_unknown_subject;
|
|
return result;
|
|
}
|
|
if (found->signature.empty()) {
|
|
result.status = trust_no_signature;
|
|
return result;
|
|
}
|
|
const unsigned char *at = found->signature.data();
|
|
std::unique_ptr<PKCS7, decltype(&PKCS7_free)> signature(d2i_PKCS7(nullptr, &at, long(found->signature.size())), PKCS7_free);
|
|
if (!signature || !PKCS7_type_is_signed(signature.get()) || !signature->d.sign->contents ||
|
|
signature->d.sign->contents->d.other == nullptr || signature->d.sign->contents->d.other->type != V_ASN1_SEQUENCE) {
|
|
result.status = trust_no_signature;
|
|
return result;
|
|
}
|
|
// SpcIndirectDataContent: what was signed, then the DigestInfo of the file's content.
|
|
const auto *content = signature->d.sign->contents->d.other->value.sequence;
|
|
const std::span<const uint8_t> indirect(content->data, size_t(content->length));
|
|
const auto outer = der_header(indirect, 0);
|
|
const auto first = outer ? der_header(indirect, outer->first) : std::nullopt;
|
|
if (!outer || !first) {
|
|
result.status = trust_no_signature;
|
|
return result;
|
|
}
|
|
const size_t digest_at = outer->first + first->first + first->second;
|
|
const unsigned char *digest_bytes = indirect.data() + digest_at;
|
|
std::unique_ptr<X509_SIG, decltype(&X509_SIG_free)> digest_info(
|
|
digest_at < indirect.size() ? d2i_X509_SIG(nullptr, &digest_bytes, long(indirect.size() - digest_at)) : nullptr, X509_SIG_free);
|
|
if (!digest_info) {
|
|
result.status = trust_no_signature;
|
|
return result;
|
|
}
|
|
const X509_ALGOR *algorithm = nullptr;
|
|
const ASN1_OCTET_STRING *expected = nullptr;
|
|
X509_SIG_get0(digest_info.get(), &algorithm, &expected);
|
|
const EVP_MD *method = EVP_get_digestbyobj(algorithm->algorithm);
|
|
if (!method) {
|
|
result.status = trust_bad_digest;
|
|
return result;
|
|
}
|
|
std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)> hashing(EVP_MD_CTX_new(), EVP_MD_CTX_free);
|
|
unsigned char computed[EVP_MAX_MD_SIZE];
|
|
unsigned int computed_size = 0;
|
|
EVP_DigestInit_ex(hashing.get(), method, nullptr);
|
|
found->hash(hashing.get());
|
|
EVP_DigestFinal_ex(hashing.get(), computed, &computed_size);
|
|
if (int(computed_size) != expected->length || std::memcmp(computed, expected->data, computed_size) != 0) {
|
|
result.status = trust_bad_digest;
|
|
return result;
|
|
}
|
|
// The signer's signature covers the content of SpcIndirectDataContent, without its own header.
|
|
std::unique_ptr<BIO, decltype(&BIO_free)> signed_data(BIO_new_mem_buf(indirect.data() + outer->first, int(indirect.size() - outer->first)), BIO_free);
|
|
if (PKCS7_verify(signature.get(), nullptr, nullptr, signed_data.get(), nullptr, PKCS7_NOVERIFY | PKCS7_BINARY) != 1) {
|
|
result.status = trust_bad_signature;
|
|
return result;
|
|
}
|
|
STACK_OF(X509) *signers = PKCS7_get0_signers(signature.get(), nullptr, 0);
|
|
X509 *signer = signers && sk_X509_num(signers) ? sk_X509_value(signers, 0) : nullptr;
|
|
PKCS7_SIGNER_INFO *signer_info = sk_PKCS7_SIGNER_INFO_value(PKCS7_get_signer_info(signature.get()), 0);
|
|
if (!signer || !signer_info) {
|
|
sk_X509_free(signers);
|
|
result.status = trust_bad_signature;
|
|
return result;
|
|
}
|
|
// The chain is judged at the time the signature was stamped, as Windows does, or now when it carries no timestamp.
|
|
std::unique_ptr<X509_STORE_CTX, decltype(&X509_STORE_CTX_free)> verify(X509_STORE_CTX_new(), X509_STORE_CTX_free);
|
|
X509_STORE_CTX_init(verify.get(), trust_anchors(), signer, signature->d.sign->cert);
|
|
X509_STORE_CTX_set_purpose(verify.get(), X509_PURPOSE_ANY);
|
|
// Old Microsoft authorities name no basic constraints; Windows does not ask a certificate without them to declare itself a CA.
|
|
X509_STORE_CTX_set_verify_cb(verify.get(), [](int accepted, X509_STORE_CTX *store) {
|
|
if (!accepted && X509_STORE_CTX_get_error(store) == X509_V_ERR_INVALID_CA)
|
|
if (X509 *certificate = X509_STORE_CTX_get_current_cert(store); certificate && !(X509_get_extension_flags(certificate) & EXFLAG_BCONS))
|
|
return 1;
|
|
return accepted;
|
|
});
|
|
const auto when = stamped_time(signer_info);
|
|
X509_VERIFY_PARAM_set_time(X509_STORE_CTX_get0_param(verify.get()),
|
|
when.value_or(time_t(std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count())));
|
|
const bool trusted = X509_verify_cert(verify.get()) == 1;
|
|
// RAVEN_TRACE_AUTHENTICODE names what the chain check found and the time it judged the signature at.
|
|
if (std::getenv("RAVEN_TRACE_AUTHENTICODE"))
|
|
std::fprintf(stderr, "authenticode: %s, stamped %lld, chain: %s\n", trusted ? "trusted" : "not trusted", when ? (long long)*when : -1LL,
|
|
trusted ? "ok" : X509_verify_cert_error_string(X509_STORE_CTX_get_error(verify.get())));
|
|
if (STACK_OF(X509) *chain = X509_STORE_CTX_get0_chain(verify.get()))
|
|
for (int index = 0; index < sk_X509_num(chain); ++index)
|
|
result.chain.push_back(der_of(sk_X509_value(chain, index)));
|
|
if (result.chain.empty())
|
|
result.chain.push_back(der_of(signer));
|
|
sk_X509_free(signers);
|
|
result.anchored = trusted;
|
|
if (!trusted) {
|
|
switch (X509_STORE_CTX_get_error(verify.get())) {
|
|
case X509_V_ERR_CERT_HAS_EXPIRED: case X509_V_ERR_CERT_NOT_YET_VALID: result.status = certificate_expired; break;
|
|
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT: case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY: case X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE:
|
|
result.status = certificate_chaining;
|
|
break;
|
|
default: result.status = certificate_untrusted_root; break;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
#else
|
|
|
|
AuthenticodeResult verify_authenticode(std::span<const uint8_t>)
|
|
{
|
|
// Without the cryptography library no signature can be checked: the file is reported as unsigned.
|
|
return {0x800b0100, {}, false};
|
|
}
|
|
|
|
std::vector<uint8_t> embedded_signature(std::span<const uint8_t>)
|
|
{
|
|
return {};
|
|
}
|
|
|
|
std::vector<uint8_t> subject_digest(std::span<const uint8_t>, bool)
|
|
{
|
|
return {};
|
|
}
|
|
|
|
#endif
|
|
|
|
}
|