Skip to content

Commit

Permalink
Merge pull request #3938 from Rohde-Schwarz/fix/x509-nesting-rules
Browse files Browse the repository at this point in the history
X509 Path Validation Flag to Ignore Root Certificate Lifetime
  • Loading branch information
FAlbertDev committed Mar 25, 2024
2 parents f03c6a9 + ce6ca27 commit 20d372e
Show file tree
Hide file tree
Showing 8 changed files with 219 additions and 8 deletions.
4 changes: 4 additions & 0 deletions src/lib/x509/cert_status.cpp
Expand Up @@ -27,6 +27,10 @@ const char* to_string(Certificate_Status_Code code) {
return "OCSP URL not available";
case Certificate_Status_Code::OCSP_SERVER_NOT_AVAILABLE:
return "OCSP server not available";
case Certificate_Status_Code::TRUSTED_CERT_NOT_YET_VALID:
return "Trusted certificate is not yet valid";
case Certificate_Status_Code::TRUSTED_CERT_HAS_EXPIRED:
return "Trusted certificate has expired";
case Certificate_Status_Code::OCSP_ISSUER_NOT_TRUSTED:
return "OCSP issuer is not trustworthy";

Expand Down
2 changes: 2 additions & 0 deletions src/lib/x509/pkix_enums.h
Expand Up @@ -33,6 +33,8 @@ enum class Certificate_Status_Code {
DN_TOO_LONG = 501,
OCSP_NO_REVOCATION_URL = 502,
OCSP_SERVER_NOT_AVAILABLE = 503,
TRUSTED_CERT_HAS_EXPIRED = 504,
TRUSTED_CERT_NOT_YET_VALID = 505,

// Errors
FIRST_ERROR_STATUS = 1000,
Expand Down
21 changes: 17 additions & 4 deletions src/lib/x509/x509path.cpp
Expand Up @@ -102,13 +102,24 @@ CertificatePathStatusCodes PKIX::check_chain(const std::vector<X509_Certificate>
}
}

// Only warn, if trusted root is not in time range if configured this way
const bool is_trusted_root_and_time_ignored =
restrictions.ignore_trusted_root_time_range() && at_self_signed_root;
// Check all certs for valid time range
if(validation_time < subject.not_before()) {
status.insert(Certificate_Status_Code::CERT_NOT_YET_VALID);
if(is_trusted_root_and_time_ignored) {
status.insert(Certificate_Status_Code::TRUSTED_CERT_NOT_YET_VALID); // only warn
} else {
status.insert(Certificate_Status_Code::CERT_NOT_YET_VALID);
}
}

if(validation_time > subject.not_after()) {
status.insert(Certificate_Status_Code::CERT_HAS_EXPIRED);
if(is_trusted_root_and_time_ignored) {
status.insert(Certificate_Status_Code::TRUSTED_CERT_HAS_EXPIRED); // only warn
} else {
status.insert(Certificate_Status_Code::CERT_HAS_EXPIRED);
}
}

// Check issuer constraints
Expand Down Expand Up @@ -945,12 +956,14 @@ Path_Validation_Restrictions::Path_Validation_Restrictions(bool require_rev,
size_t key_strength,
bool ocsp_intermediates,
std::chrono::seconds max_ocsp_age,
std::unique_ptr<Certificate_Store> trusted_ocsp_responders) :
std::unique_ptr<Certificate_Store> trusted_ocsp_responders,
bool ignore_trusted_root_time_range) :
m_require_revocation_information(require_rev),
m_ocsp_all_intermediates(ocsp_intermediates),
m_minimum_key_strength(key_strength),
m_max_ocsp_age(max_ocsp_age),
m_trusted_ocsp_responders(std::move(trusted_ocsp_responders)) {
m_trusted_ocsp_responders(std::move(trusted_ocsp_responders)),
m_ignore_trusted_root_time_range(ignore_trusted_root_time_range) {
if(key_strength <= 80) {
m_trusted_hashes.insert("SHA-1");
}
Expand Down
31 changes: 27 additions & 4 deletions src/lib/x509/x509path.h
Expand Up @@ -50,13 +50,16 @@ class BOTAN_PUBLIC_API(2, 0) Path_Validation_Restrictions final {
* If zero, there is no maximum age
* @param trusted_ocsp_responders certificate store containing certificates
* of trusted OCSP responders (additionally to the CA's responders)
* @param ignore_trusted_root_time_range if true, validity checks on the
* time range of the trusted root certificate only produce warnings
*/
Path_Validation_Restrictions(
bool require_rev = false,
size_t minimum_key_strength = 110,
bool ocsp_all_intermediates = false,
std::chrono::seconds max_ocsp_age = std::chrono::seconds::zero(),
std::unique_ptr<Certificate_Store> trusted_ocsp_responders = std::make_unique<Certificate_Store_In_Memory>());
std::unique_ptr<Certificate_Store> trusted_ocsp_responders = std::make_unique<Certificate_Store_In_Memory>(),
bool ignore_trusted_root_time_range = false);

/**
* @param require_rev if true, revocation information is required
Expand All @@ -72,20 +75,24 @@ class BOTAN_PUBLIC_API(2, 0) Path_Validation_Restrictions final {
* If zero, there is no maximum age
* @param trusted_ocsp_responders certificate store containing certificates
* of trusted OCSP responders (additionally to the CA's responders)
* @param ignore_trusted_root_time_range if true, validity checks on the
* time range of the trusted root certificate only produce warnings
*/
Path_Validation_Restrictions(
bool require_rev,
size_t minimum_key_strength,
bool ocsp_all_intermediates,
const std::set<std::string>& trusted_hashes,
std::chrono::seconds max_ocsp_age = std::chrono::seconds::zero(),
std::unique_ptr<Certificate_Store> trusted_ocsp_responders = std::make_unique<Certificate_Store_In_Memory>()) :
std::unique_ptr<Certificate_Store> trusted_ocsp_responders = std::make_unique<Certificate_Store_In_Memory>(),
bool ignore_trusted_root_time_range = false) :
m_require_revocation_information(require_rev),
m_ocsp_all_intermediates(ocsp_all_intermediates),
m_trusted_hashes(trusted_hashes),
m_minimum_key_strength(minimum_key_strength),
m_max_ocsp_age(max_ocsp_age),
m_trusted_ocsp_responders(std::move(trusted_ocsp_responders)) {}
m_trusted_ocsp_responders(std::move(trusted_ocsp_responders)),
m_ignore_trusted_root_time_range(ignore_trusted_root_time_range) {}

/**
* @return whether revocation information is required
Expand Down Expand Up @@ -121,13 +128,27 @@ class BOTAN_PUBLIC_API(2, 0) Path_Validation_Restrictions final {
*/
const Certificate_Store* trusted_ocsp_responders() const { return m_trusted_ocsp_responders.get(); }

/**
* RFC 5280 does not disallow trusted anchors signing certificates with wider validity
* ranges than theirs. When checking a certificate chain at a specific
* point in time, this can lead to situations where a root certificate is expired, but
* the lower-chain certificates are not.
*
* If this flag is set to true, such chains are considered valid (with warning
* TRUSTED_CERT_HAS_EXPIRED). Otherwise, the chain is rejected with the error
* code CERT_HAS_EXPIRED. The same holds for not yet valid certificates with the
* error code CERT_NOT_YET_VALID (or warning TRUSTED_CERT_NOT_YET_VALID).
*/
bool ignore_trusted_root_time_range() const { return m_ignore_trusted_root_time_range; }

private:
bool m_require_revocation_information;
bool m_ocsp_all_intermediates;
std::set<std::string> m_trusted_hashes;
size_t m_minimum_key_strength;
std::chrono::seconds m_max_ocsp_age;
std::unique_ptr<Certificate_Store> m_trusted_ocsp_responders;
bool m_ignore_trusted_root_time_range;
};

/**
Expand Down Expand Up @@ -331,7 +352,9 @@ Certificate_Status_Code BOTAN_PUBLIC_API(2, 0)
/**
* Check the certificate chain, but not any revocation data
*
* @param cert_path path built by build_certificate_path with OK result
* @param cert_path path built by build_certificate_path with OK result.
* The first element is the end entity certificate, the last element is
* the trusted root certificate.
* @param ref_time whatever time you want to perform the validation
* against (normally current system clock)
* @param hostname the hostname
Expand Down
17 changes: 17 additions & 0 deletions src/tests/data/x509/misc/root_cert_time_check/README.md
@@ -0,0 +1,17 @@
# Test: Root Certificate Time Check
RFC 5280 does not disallow CAs to sign certificates with wider validity
ranges than theirs. When checking a certificate chain at a specific
point in time, this can lead to situations where a CA is expired or not
yet valid, but the end-entity certificate is in the validity range.

Botan provides an option to decide if such cases are considered valid.

## Test Certificates
This test case contains two certificates:
- A trusted root certificate `root.crt`. Validity range (years): 2022-2028.
- An end-entity certificate `leaf.crt` chaining to `root.crt`.
Validity range (years): 2020-2030.

These certificates are used to test Botan's behavior for verification at
specific time points. For example, verification in 2025 succeeds,
verification in 2031 fails, and verification at 2029 depends on the option.
25 changes: 25 additions & 0 deletions src/tests/data/x509/misc/root_cert_time_check/leaf.crt
@@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIELDCCAxagAwIBAgIRAJ9N52O3Ju8BygPBNJxwolUwCwYJKoZIhvcNAQELMIGN
MScwJQYDVQQDEx5mcm9kby5yaW5nLWRlcG9zYWwtc2VydmljZS5jb20xCzAJBgNV
BAYTAk1FMRAwDgYDVQQIEwdFcmlhZG9yMQ4wDAYDVQQHEwVTaGlyZTEXMBUGA1UE
ChMOSG9iYml0IFNvY2lldHkxGjAYBgNVBAsTEVJpbmcgRGVwb3NhbCBVbml0MB4X
DTIwMDEwMTAwMDAwMFoXDTMwMDEwMTAwMDAwMFowgY8xKTAnBgNVBAMTIHNhbXdp
c2UucmluZy1kZXBvc2FsLXNlcnZpY2UuY29tMQswCQYDVQQGEwJNRTEQMA4GA1UE
CBMHRXJpYWRvcjEOMAwGA1UEBxMFU2hpcmUxFzAVBgNVBAoTDkhvYmJpdCBTb2Np
ZXR5MRowGAYDVQQLExFSaW5nIERlcG9zYWwgVW5pdDCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBAJpQbqcw4Q0xyMeI9mTjYLT0SZELXs1BTMLD6tJlFdqP
x+2gmQLv5gauRWReGVWqmJ6SUUWAKnfQQe/zeMpRJgNSvk0tyVuVj8PhUpLo4xxk
/wyfJSArsk1ppml8PTOgROLFS/2fmrs8Qm+0sTLgqWNNf0p7fnQTcm39AqzmFEqz
Dy24rIAbEN+vCg9Rm67meYlDvtkaAyQGAIevfJJiytDMt2/jW8Vu4Rwul1fc/28e
by3v8E/tr2Zk1zMTe9JqdKbegGjkNWekO5Ny3EDjBUCR+xAXZ3aiyAOWn1fEZaxK
vlO44tvU+Yuu1M7K8P9Dl+bnA1CTquhCJY1Joj93FDkCAwEAAaOBhjCBgzAhBgNV
HQ4EGgQYBLOa+3C4xhfo+tQ1fVJPiE0lnDIFtJ0NMCsGA1UdEQQkMCKBIHNhbXdp
c2VAcmluZy1kZXBvc2FsLXNlcnZpY2UuY29tMAwGA1UdEwEB/wQCMAAwIwYDVR0j
BBwwGoAYHh+1BXEVqx6agPTcDw8lW9jm65pxUmOlMAsGCSqGSIb3DQEBCwOCAQEA
FnuHOYgcYI8JqnXtLP+Be1yYIU++HA4njmfB3iCcVwIRa/iRVhD7lb9L5oepeCkR
Y3aG3IbI8UZhhckMf6qNeJa1bJhhVxzjUmyF6QDVLAswFex+L6UMZXOS4SmE0wxF
KKTuEPuSLWtmwLtf25Skt7HS/NsJpqtBPt1VpwaGYRX8Lkj+20IeFyYykIfqHMyW
DqWzjSPYFZ2B7Bi/xct5+snorGUq8BzWxVKGVQwvBvO0eU1nkGSydRhcLB0qJOoI
9kgBXWp2XtX5rxoDQRUTf/rzw2xBsUQgcuE+HFYEFOfSc7d2CUNiBogm5ogYaJw0
UUTCQwN7n/uvD+E+CC5UYA==
-----END CERTIFICATE-----
25 changes: 25 additions & 0 deletions src/tests/data/x509/misc/root_cert_time_check/root.crt
@@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEPjCCAyigAwIBAgIRANrPF/sE7W2NfzY92hX5hs8wCwYJKoZIhvcNAQELMIGN
MScwJQYDVQQDEx5mcm9kby5yaW5nLWRlcG9zYWwtc2VydmljZS5jb20xCzAJBgNV
BAYTAk1FMRAwDgYDVQQIEwdFcmlhZG9yMQ4wDAYDVQQHEwVTaGlyZTEXMBUGA1UE
ChMOSG9iYml0IFNvY2lldHkxGjAYBgNVBAsTEVJpbmcgRGVwb3NhbCBVbml0MB4X
DTIyMDEwMTAwMDAwMFoXDTI4MDEwMTAwMDAwMFowgY0xJzAlBgNVBAMTHmZyb2Rv
LnJpbmctZGVwb3NhbC1zZXJ2aWNlLmNvbTELMAkGA1UEBhMCTUUxEDAOBgNVBAgT
B0VyaWFkb3IxDjAMBgNVBAcTBVNoaXJlMRcwFQYDVQQKEw5Ib2JiaXQgU29jaWV0
eTEaMBgGA1UECxMRUmluZyBEZXBvc2FsIFVuaXQwggEiMA0GCSqGSIb3DQEBAQUA
A4IBDwAwggEKAoIBAQDF7NhPUVyVvkN1VznTd67apvqJYxhYTmtTmt53qPR9EavL
7lA63eVNYH8mMr/ovvniNsrg57UGGxmtmKchsFmH6N8L26vZV9RZeu6DQR6Ae5K0
cUA7GbYFQ8d0LM2UkD57JMVtx1xK8QYInLcvRbxUWJGkhH+TgAzD7tAKzIm/Uyhv
6bKJaPLt6fYKmeXw/oEFdUdqkMAbH+jbiMy4JiGvzA7t2v4i+KafwM/Vq+6GOfyN
y6eMW/tnQQWNxI5GWPsw/xDiy3Z8ihuucbrvFR9e6S95s3EFW9hvNsjYl7yuJYgA
nG46OUPJ1/6+yRJJ+UHu/vbUORGoKHGYFN0TYscvAgMBAAGjgZowgZcwIQYDVR0O
BBoEGB4ftQVxFasemoD03A8PJVvY5uuacVJjpTAOBgNVHQ8BAf8EBAMCAQYwKQYD
VR0RBCIwIIEeZnJvZG9AcmluZy1kZXBvc2FsLXNlcnZpY2UuY29tMBIGA1UdEwEB
/wQIMAYBAf8CASowIwYDVR0jBBwwGoAYHh+1BXEVqx6agPTcDw8lW9jm65pxUmOl
MAsGCSqGSIb3DQEBCwOCAQEAuq6wuyBJ0QMx2yCXbEIlp3HMF1/ebR8HMv1mUrow
adwSrS2BLq78tGH7OVbdz6PCUWjQ/Wx9u10MBBlLr8vaaD3W8sYFCaIQItKzRsqQ
M3mzRJJ5gZnaphVGkHZCviODXkqI9OVxrpAS9FILUMpa5fajmlDNkj9+I0P7823G
7IkC5sbShxzha/7abm3layNXlIS3n//nEVSNtchhysO1aWzZ8nH6FdhD4rP0MQEq
PTSviLVhHlbkwhlA1W5pS3Dp8tYYFKskPTTCb6alwfKC0ic8fwQHdZ5hhronb1B0
Vsv/qTDfpQpDzCzyCoeSfoxmLrKr4ISHTeEQU49pCEvU5w==
-----END CERTIFICATE-----
102 changes: 102 additions & 0 deletions src/tests/test_x509_path.cpp
Expand Up @@ -631,6 +631,108 @@ std::vector<Test::Result> Validate_Name_Constraint_NoCheckSelf::run() {

BOTAN_REGISTER_TEST("x509", "x509_name_constraint_no_check_self", Validate_Name_Constraint_NoCheckSelf);

class Root_Cert_Time_Check_Test final : public Test {
public:
std::vector<Test::Result> run() override {
if(Botan::has_filesystem_impl() == false) {
return {Test::Result::Note("Path validation", "Skipping due to missing filesystem access")};
}

const std::string trusted_root_crt = Test::data_file("/x509/misc/root_cert_time_check/root.crt");
const std::string leaf_crt = Test::data_file("/x509/misc/root_cert_time_check/leaf.crt");

const Botan::X509_Certificate trusted_root_cert(trusted_root_crt);
const Botan::X509_Certificate leaf_cert(leaf_crt);

Botan::Certificate_Store_In_Memory trusted;
trusted.add_certificate(trusted_root_cert);

const std::vector<Botan::X509_Certificate> chain = {leaf_cert};

Test::Result result("Root cert time check");

auto assert_path_validation_result = [&](std::string_view descr,
bool ignore_trusted_root_time_range,
uint32_t year,
Botan::Certificate_Status_Code exp_status,
std::optional<Botan::Certificate_Status_Code> exp_warning =
std::nullopt) {
const Botan::Path_Validation_Restrictions restrictions(
false,
110,
false,
std::chrono::seconds::zero(),
std::make_unique<Botan::Certificate_Store_In_Memory>(),
ignore_trusted_root_time_range);

const Botan::Path_Validation_Result validation_result =
Botan::x509_path_validate(chain,
restrictions,
trusted,
"",
Botan::Usage_Type::UNSPECIFIED,
Botan::calendar_point(year, 1, 1, 1, 0, 0).to_std_timepoint());
const std::string descr_str = Botan::fmt(
"Root cert validity range {}: {}", ignore_trusted_root_time_range ? "ignored" : "checked", descr);

result.test_is_eq(descr_str, validation_result.result(), exp_status);
const auto warnings = validation_result.warnings();
BOTAN_ASSERT_NOMSG(warnings.size() == 2);
result.confirm("No warning for leaf cert", warnings.at(0).empty());
if(exp_warning) {
result.confirm("Warning for root cert",
warnings.at(1).size() == 1 && warnings.at(1).contains(*exp_warning));
} else {
result.confirm("No warning for root cert", warnings.at(1).empty());
}
};
// (Trusted) root cert validity range: 2022-2028
// Leaf cert validity range: 2020-2030

// Trusted root time range is checked
assert_path_validation_result(
"Root and leaf certs in validity range", false, 2025, Botan::Certificate_Status_Code::OK);
assert_path_validation_result(
"Root and leaf certs are expired", false, 2031, Botan::Certificate_Status_Code::CERT_HAS_EXPIRED);
assert_path_validation_result(
"Root and leaf certs are not yet valid", false, 2019, Botan::Certificate_Status_Code::CERT_NOT_YET_VALID);
assert_path_validation_result(
"Root cert is expired, leaf cert not", false, 2029, Botan::Certificate_Status_Code::CERT_HAS_EXPIRED);
assert_path_validation_result("Root cert is not yet valid, leaf cert is",
false,
2021,
Botan::Certificate_Status_Code::CERT_NOT_YET_VALID);

// Trusted root time range is ignored
assert_path_validation_result(
"Root and leaf certs in validity range", true, 2025, Botan::Certificate_Status_Code::OK);
assert_path_validation_result("Root and leaf certs are expired",
true,
2031,
Botan::Certificate_Status_Code::CERT_HAS_EXPIRED,
Botan::Certificate_Status_Code::TRUSTED_CERT_HAS_EXPIRED);
assert_path_validation_result("Root and leaf certs are not yet valid",
true,
2019,
Botan::Certificate_Status_Code::CERT_NOT_YET_VALID,
Botan::Certificate_Status_Code::TRUSTED_CERT_NOT_YET_VALID);
assert_path_validation_result("Root cert is expired, leaf cert not",
true,
2029,
Botan::Certificate_Status_Code::OK,
Botan::Certificate_Status_Code::TRUSTED_CERT_HAS_EXPIRED);
assert_path_validation_result("Root cert is not yet valid, leaf cert is",
true,
2021,
Botan::Certificate_Status_Code::OK,
Botan::Certificate_Status_Code::TRUSTED_CERT_NOT_YET_VALID);

return {result};
}
};

BOTAN_REGISTER_TEST("x509", "x509_root_cert_time_check", Root_Cert_Time_Check_Test);

class BSI_Path_Validation_Tests final : public Test

{
Expand Down

0 comments on commit 20d372e

Please sign in to comment.