Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 45f2f4b

Browse files
authored
Add secureboot_certificates table for Linux (#8844)
Based on an original Python prototype from @pboushy in fleetdm/fleet#43684
1 parent 06603a8 commit 45f2f4b

8 files changed

Lines changed: 729 additions & 0 deletions

File tree

osquery/tables/system/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ function(generateOsqueryTablesSystemSystemtable)
7979
linux/processes.cpp
8080
linux/rpm_packages.cpp
8181
linux/secureboot.cpp
82+
linux/secureboot_certificates.cpp
8283
linux/shadow.cpp
8384
linux/shared_memory.cpp
8485
linux/smbios_tables.cpp
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
/**
2+
* Copyright (c) 2014-present, The osquery authors
3+
*
4+
* This source code is licensed as defined by the LICENSE file found in the
5+
* root directory of this source tree.
6+
*
7+
* SPDX-License-Identifier: (Apache-2.0 OR GPL-2.0-only)
8+
*/
9+
10+
#include <osquery/core/tables.h>
11+
#include <osquery/filesystem/filesystem.h>
12+
#include <osquery/logger/logger.h>
13+
#include <osquery/tables/system/posix/openssl_utils.h>
14+
#include <osquery/utils/expected/expected.h>
15+
16+
#include <openssl/x509.h>
17+
18+
#include <boost/endian/conversion.hpp>
19+
#include <boost/optional.hpp>
20+
21+
#include <cstdint>
22+
#include <iterator>
23+
#include <string>
24+
#include <vector>
25+
26+
namespace osquery {
27+
namespace tables {
28+
29+
namespace {
30+
// The first 4 bytes of an efivars file are EFI variable attributes, not data.
31+
static constexpr std::size_t kEfiVarAttributeSize = 4U;
32+
33+
// EFI_SIGNATURE_LIST header layout (28 bytes total):
34+
// [0..15] SignatureType GUID
35+
// [16..19] SignatureListSize (LE uint32)
36+
// [20..23] SignatureHeaderSize (LE uint32)
37+
// [24..27] SignatureSize (LE uint32)
38+
static constexpr std::size_t kEslHeaderSize = 28U;
39+
40+
// Each EFI_SIGNATURE_DATA entry is prefixed with a 16-byte SignatureOwner GUID
41+
// before the actual signature payload (e.g. DER certificate bytes).
42+
static constexpr std::size_t kSignatureOwnerGuidSize = 16U;
43+
44+
// First byte of EFI_CERT_X509_GUID as stored on disk in little-endian order.
45+
// EFI_CERT_X509_GUID = {0xa5c059a1, ...} -> disk byte 0 == 0xa1
46+
static constexpr uint8_t kX509GuidFirstByte = 0xa1U;
47+
48+
// Minimum ESL payload size we consider meaningful.
49+
static constexpr std::size_t kMinEslSize = 28U;
50+
51+
// EFI secure boot signature stores to read, with their canonical store names.
52+
// EFI_IMAGE_SECURITY_DATABASE_GUID = d719b2cb-3d3a-4596-a3bc-dad00e67656f
53+
static const std::vector<std::pair<std::string, std::string>> kEfiDbGlobs{
54+
{"/sys/firmware/efi/efivars/db-*", "db"},
55+
{"/sys/firmware/efi/efivars/dbx-*", "dbx"},
56+
};
57+
58+
using X509Deleter = void (*)(X509*);
59+
using UniqueX509 = std::unique_ptr<X509, X509Deleter>;
60+
61+
struct EfiCertInfo {
62+
std::string common_name;
63+
std::string subject;
64+
std::string issuer;
65+
std::time_t not_valid_before{0};
66+
std::time_t not_valid_after{0};
67+
std::string sha1;
68+
std::string serial;
69+
bool is_ca{false};
70+
bool is_self_signed{false};
71+
std::string key_usage;
72+
std::string authority_key_id;
73+
std::string subject_key_id;
74+
std::string signing_algorithm;
75+
std::string key_algorithm;
76+
std::string key_strength;
77+
};
78+
79+
} // namespace
80+
81+
boost::optional<EfiCertInfo> parseDerCertificate(const uint8_t* data,
82+
std::size_t len) {
83+
const uint8_t* ptr = data;
84+
UniqueX509 cert(d2i_X509(nullptr, &ptr, static_cast<long>(len)), X509_free);
85+
if (!cert) {
86+
return boost::none;
87+
}
88+
89+
EfiCertInfo info;
90+
91+
auto opt_common_name = getCertificateCommonName(cert.get());
92+
if (opt_common_name.has_value()) {
93+
info.common_name = opt_common_name.value();
94+
}
95+
96+
auto opt_subject = getCertificateSubjectName(cert.get(), true);
97+
if (opt_subject.has_value()) {
98+
info.subject = opt_subject.value();
99+
}
100+
101+
auto opt_issuer = getCertificateIssuerName(cert.get(), true);
102+
if (opt_issuer.has_value()) {
103+
info.issuer = opt_issuer.value();
104+
}
105+
106+
auto opt_not_valid_before = getCertificateNotValidBefore(cert.get());
107+
if (opt_not_valid_before.has_value()) {
108+
info.not_valid_before = opt_not_valid_before.value();
109+
}
110+
111+
auto opt_not_valid_after = getCertificateNotValidAfter(cert.get());
112+
if (opt_not_valid_after.has_value()) {
113+
info.not_valid_after = opt_not_valid_after.value();
114+
}
115+
116+
auto opt_sha1 = generateCertificateSHA1Digest(cert.get());
117+
if (opt_sha1.has_value()) {
118+
info.sha1 = opt_sha1.value();
119+
}
120+
121+
auto opt_serial = getCertificateSerialNumber(cert.get());
122+
if (opt_serial.has_value()) {
123+
info.serial = opt_serial.value();
124+
}
125+
126+
getCertificateAttributes(cert.get(), info.is_ca, info.is_self_signed);
127+
128+
auto opt_key_usage = getCertificateKeyUsage(cert.get());
129+
if (opt_key_usage.has_value()) {
130+
info.key_usage = opt_key_usage.value();
131+
}
132+
133+
auto opt_authority_key_id = getCertificateAuthorityKeyID(cert.get());
134+
if (opt_authority_key_id.has_value()) {
135+
info.authority_key_id = opt_authority_key_id.value();
136+
}
137+
138+
auto opt_subject_key_id = getCertificateSubjectKeyID(cert.get());
139+
if (opt_subject_key_id.has_value()) {
140+
info.subject_key_id = opt_subject_key_id.value();
141+
}
142+
143+
auto opt_signing_algorithm = getCertificateSigningAlgorithm(cert.get());
144+
if (opt_signing_algorithm.has_value()) {
145+
info.signing_algorithm = opt_signing_algorithm.value();
146+
}
147+
148+
auto opt_key_algorithm = getCertificateKeyAlgorithm(cert.get());
149+
if (opt_key_algorithm.has_value()) {
150+
info.key_algorithm = opt_key_algorithm.value();
151+
}
152+
153+
auto opt_key_strength = getCertificateKeyStregth(cert.get());
154+
if (opt_key_strength.has_value()) {
155+
info.key_strength = opt_key_strength.value();
156+
}
157+
158+
return info;
159+
}
160+
161+
// Parse a sequence of concatenated EFI_SIGNATURE_LIST structures and add any
162+
// X.509 certificates found to results.
163+
void parseEslData(const std::string& content,
164+
bool revoked,
165+
const std::string& path,
166+
QueryData& results) {
167+
if (content.size() < kEfiVarAttributeSize + kMinEslSize) {
168+
VLOG(1) << "secureboot_certificates: ESL data too short in " << path;
169+
return;
170+
}
171+
172+
const uint8_t* data = reinterpret_cast<const uint8_t*>(content.data());
173+
std::size_t head = kEfiVarAttributeSize;
174+
const std::size_t total = content.size();
175+
176+
while (head < total) {
177+
if (head + kEslHeaderSize > total) {
178+
break;
179+
}
180+
181+
const uint32_t sig_list_size =
182+
boost::endian::load_little_u32(&data[head + 16]);
183+
const uint32_t sig_header_size =
184+
boost::endian::load_little_u32(&data[head + 20]);
185+
const uint32_t sig_size = boost::endian::load_little_u32(&data[head + 24]);
186+
187+
// Validate sizes before advancing
188+
if (sig_list_size == 0 || head + sig_list_size > total) {
189+
VLOG(1) << "secureboot_certificates: Invalid sig_list_size at offset "
190+
<< head << " in " << path;
191+
break;
192+
}
193+
194+
// Only process X.509 certificate entries; skip hash entries and others.
195+
if (data[head] != kX509GuidFirstByte) {
196+
head += sig_list_size;
197+
continue;
198+
}
199+
200+
// Each EFI_SIGNATURE_DATA entry is sig_size bytes, prefixed with a 16-byte
201+
// owner GUID. Guard against malformed data where sig_size is too small.
202+
if (sig_size <= kSignatureOwnerGuidSize) {
203+
head += sig_list_size;
204+
continue;
205+
}
206+
207+
// The signature data area follows the fixed header and optional
208+
// SignatureHeader.
209+
const std::size_t sigs_area_offset =
210+
head + kEslHeaderSize + sig_header_size;
211+
if (sigs_area_offset > head + sig_list_size) {
212+
head += sig_list_size;
213+
continue;
214+
}
215+
216+
const std::size_t sigs_area_size =
217+
(head + sig_list_size) - sigs_area_offset;
218+
const std::size_t num_sigs = sigs_area_size / sig_size;
219+
220+
for (std::size_t i = 0; i < num_sigs; ++i) {
221+
const std::size_t sig_offset = sigs_area_offset + (i * sig_size);
222+
const std::size_t cert_offset = sig_offset + kSignatureOwnerGuidSize;
223+
const std::size_t cert_size = sig_size - kSignatureOwnerGuidSize;
224+
225+
if (cert_offset + cert_size > total) {
226+
break;
227+
}
228+
229+
auto opt_cert_info = parseDerCertificate(&data[cert_offset], cert_size);
230+
if (!opt_cert_info.has_value()) {
231+
VLOG(1) << "secureboot_certificates: Failed to parse DER certificate "
232+
"at offset "
233+
<< cert_offset << " in " << path;
234+
continue;
235+
}
236+
237+
const auto& cert_info = opt_cert_info.value();
238+
239+
Row row;
240+
row["common_name"] = SQL_TEXT(cert_info.common_name);
241+
row["subject"] = SQL_TEXT(cert_info.subject);
242+
row["issuer"] = SQL_TEXT(cert_info.issuer);
243+
row["not_valid_before"] = INTEGER(cert_info.not_valid_before);
244+
row["not_valid_after"] = INTEGER(cert_info.not_valid_after);
245+
row["sha1"] = SQL_TEXT(cert_info.sha1);
246+
row["serial"] = SQL_TEXT(cert_info.serial);
247+
row["revoked"] = INTEGER(revoked ? 1 : 0);
248+
row["path"] = SQL_TEXT(path);
249+
row["is_ca"] = INTEGER(cert_info.is_ca ? 1 : 0);
250+
row["self_signed"] = INTEGER(cert_info.is_self_signed ? 1 : 0);
251+
row["key_usage"] = SQL_TEXT(cert_info.key_usage);
252+
row["authority_key_id"] = SQL_TEXT(cert_info.authority_key_id);
253+
row["subject_key_id"] = SQL_TEXT(cert_info.subject_key_id);
254+
row["signing_algorithm"] = SQL_TEXT(cert_info.signing_algorithm);
255+
row["key_algorithm"] = SQL_TEXT(cert_info.key_algorithm);
256+
row["key_strength"] = SQL_TEXT(cert_info.key_strength);
257+
258+
results.push_back(std::move(row));
259+
}
260+
261+
head += sig_list_size;
262+
}
263+
}
264+
265+
QueryData genSecureBootCertificates(QueryContext& context) {
266+
QueryData results;
267+
268+
for (const auto& [glob_pattern, store_name] : kEfiDbGlobs) {
269+
std::vector<std::string> matching_paths;
270+
auto status = resolveFilePattern(glob_pattern, matching_paths);
271+
if (!status.ok() || matching_paths.empty()) {
272+
continue;
273+
}
274+
275+
for (const auto& efi_path : matching_paths) {
276+
std::string content;
277+
if (!readFile(efi_path, content).ok()) {
278+
VLOG(1) << "secureboot_certificates: Cannot open " << efi_path;
279+
continue;
280+
}
281+
282+
if (content.size() <= kEfiVarAttributeSize) {
283+
continue;
284+
}
285+
286+
parseEslData(content, store_name == "dbx", efi_path, results);
287+
}
288+
}
289+
290+
return results;
291+
}
292+
293+
} // namespace tables
294+
} // namespace osquery

osquery/tables/system/tests/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ function(generateOsqueryTablesSystemLinuxTests)
3737
linux/portage_tests.cpp
3838
linux/processes_tests.cpp
3939
linux/rpm_packages_tests.cpp
40+
linux/secureboot_certificates_tests.cpp
4041
linux/selinux_settings_tests.cpp
4142
linux/yum_sources_tests.cpp
4243
)
@@ -55,6 +56,7 @@ function(generateOsqueryTablesSystemLinuxTests)
5556
osquery_utils_conversions
5657
tests_helper
5758
thirdparty_googletest
59+
thirdparty_openssl
5860
)
5961
endfunction()
6062

0 commit comments

Comments
 (0)