diff --git a/doc/api_ref/ffi.rst b/doc/api_ref/ffi.rst index 84b04a2d13d..5c5229d584c 100644 --- a/doc/api_ref/ffi.rst +++ b/doc/api_ref/ffi.rst @@ -2021,6 +2021,52 @@ X.509 Certificate Revocation Lists Load a CRL from a file. +.. cpp:function:: int botan_x509_crl_create(botan_x509_crl_t* crl_obj, \ + botan_rng_t rng, \ + botan_x509_cert_t ca_cert, \ + botan_privkey_t ca_key, \ + uint64_t issue_time, \ + uint32_t next_update, \ + const char* hash_fn, \ + const char* padding) + + Create a new CRL. ``issue_time`` is expected to be a UNIX timestamp, in seconds. + ``next_update`` is the number of seconds after ``issue_time`` until the CRL expires. + ``hash_fn`` and ``padding`` may be NULL. + +.. cpp:enum:: botan_x509_crl_reason_code + + CRL revocation reason codes. Allowed values: `BOTAN_CRL_ENTRY_UNSPECIFIED`, + `BOTAN_CRL_ENTRY_KEY_COMPROMISE`, `BOTAN_CRL_ENTRY_CA_COMPROMISE`, `BOTAN_CRL_ENTRY_AFFILIATION_CHANGED`, + `BOTAN_CRL_ENTRY_SUPERSEDED`, `BOTAN_CRL_ENTRY_CESSATION_OF_OPERATION`, `BOTAN_CRL_ENTRY_CERTIFICATE_HOLD`, + `BOTAN_CRL_ENTRY_REMOVE_FROM_CRL`, `BOTAN_CRL_ENTRY_PRIVILEGE_WITHDRAWN`, `BOTAN_CRL_ENTRY_AA_COMPROMISE`. + +.. cpp:function:: int botan_x509_crl_entry_create(botan_x509_crl_entry_t* entry, botan_x509_cert_t cert, int reason_code) + + Create a new CRL entry to be added to a CRL later. + +.. cpp:function:: int botan_x509_crl_update(botan_x509_crl_t* crl_obj, \ + botan_x509_crl_t last_crl, \ + botan_rng_t rng, \ + botan_x509_cert_t ca_cert, \ + botan_privkey_t ca_key, \ + uint64_t issue_time, \ + uint32_t next_update, \ + const botan_x509_crl_entry_t* new_entries, \ + size_t new_entries_len, \ + const char* hash_fn, \ + const char* padding) + + Revoke some certificates. This does not update the given CRL in place. + ``issue_time`` is expected to be a UNIX timestamp, in seconds. + ``next_update`` is the number of seconds after ``issue_time`` until the CRL expires. + ``hash_fn`` and ``padding`` may be NULL. + ``new_entries`` is an array of ``botan_x509_crl_entry_t`` objects, ``new_entries_len`` is its length. + +.. cpp:function:: int botan_x509_crl_verify_signature(botan_x509_crl_t crl, botan_pubkey_t key) + + Verify the signature of a CRL. Returns 1 if the signature is valid, 0 otherwise. + .. cpp:function:: int botan_x509_crl_destroy(botan_x509_crl_t crl) Destroy the CRL object. @@ -2101,7 +2147,7 @@ X.509 Certificate Revocation Lists .. cpp:function:: int botan_x509_crl_entry_reason(botan_x509_crl_entry_t entry, int* reason_code) Get the revocation reason code for the given CRL entry. The reason code is - according to `RFC 5280 - 5.3.1 `_. + according to `RFC 5280 - 5.3.1 `, see :cpp:enum:`botan_x509_crl_reason_code`. .. cpp:function:: int botan_x509_crl_entry_revocation_date(botan_x509_crl_entry_t entry, uint64_t* time_since_epoch) diff --git a/src/lib/ffi/ffi.h b/src/lib/ffi/ffi.h index 83662355759..22ffe79386b 100644 --- a/src/lib/ffi/ffi.h +++ b/src/lib/ffi/ffi.h @@ -2550,6 +2550,79 @@ int botan_x509_crl_load(botan_x509_crl_t* crl_obj, const uint8_t crl_bits[], siz BOTAN_FFI_EXPORT(3, 11) int botan_x509_crl_this_update(botan_x509_crl_t crl, uint64_t* time_since_epoch); BOTAN_FFI_EXPORT(3, 11) int botan_x509_crl_next_update(botan_x509_crl_t crl, uint64_t* time_since_epoch); +/** +* Create a new CRL +* @param crl_obj The newly created CRL +* @param rng a random number generator object +* @param ca_cert The CA Certificate the CRL belongs to +* @param ca_key The private key of that CA +* @param issue_time The time when the CRL becomes valid +* @param next_update The number of seconds after issue_time until the CRL expires +* @param hash_fn The hash function to use, may be null +* @param padding The padding to use, may be null +*/ +BOTAN_FFI_EXPORT(3, 11) +int botan_x509_crl_create(botan_x509_crl_t* crl_obj, + botan_rng_t rng, + botan_x509_cert_t ca_cert, + botan_privkey_t ca_key, + uint64_t issue_time, + uint32_t next_update, + const char* hash_fn, + const char* padding); + +/* Must match values of CRL_Code in pkix_enums.h */ +enum botan_x509_crl_reason_code /* NOLINT(*-enum-size) */ { + BOTAN_CRL_ENTRY_UNSPECIFIED = 0, + BOTAN_CRL_ENTRY_KEY_COMPROMISE = 1, + BOTAN_CRL_ENTRY_CA_COMPROMISE = 2, + BOTAN_CRL_ENTRY_AFFILIATION_CHANGED = 3, + BOTAN_CRL_ENTRY_SUPERSEDED = 4, + BOTAN_CRL_ENTRY_CESSATION_OF_OPERATION = 5, + BOTAN_CRL_ENTRY_CERTIFICATE_HOLD = 6, + BOTAN_CRL_ENTRY_REMOVE_FROM_CRL = 8, + BOTAN_CRL_ENTRY_PRIVILEGE_WITHDRAWN = 9, + BOTAN_CRL_ENTRY_AA_COMPROMISE = 10 +}; + +/** +* Create a new CRL entry that marks @p cert as revoked +* @param entry The newly created CRL entry +* @param cert The certificate to mark as revoked +* @param reason_code The reason code for revocation +*/ +BOTAN_FFI_EXPORT(3, 11) +int botan_x509_crl_entry_create(botan_x509_crl_entry_t* entry, botan_x509_cert_t cert, int reason_code); + +/** +* Update a CRL with new revoked entries. This does not modify the old crl, and instead creates a new one. +* @param crl_obj The newly created CRL +* @param last_crl The CRL to update +* @param rng a random number generator object +* @param ca_cert The CA Certificate the CRL belongs to +* @param ca_key The private key of that CA +* @param issue_time The time when the CRL becomes valid +* @param next_update The number of seconds after issue_time until the CRL expires +* @param new_entries The entries to add to the CRL +* @param new_entries_len The number of entries +* @param hash_fn The hash function to use, may be null +* @param padding The padding to use, may be null +*/ +BOTAN_FFI_EXPORT(3, 11) +int botan_x509_crl_update(botan_x509_crl_t* crl_obj, + botan_x509_crl_t last_crl, + botan_rng_t rng, + botan_x509_cert_t ca_cert, + botan_privkey_t ca_key, + uint64_t issue_time, + uint32_t next_update, + const botan_x509_crl_entry_t* new_entries, + size_t new_entries_len, + const char* hash_fn, + const char* padding); + +BOTAN_FFI_EXPORT(3, 11) int botan_x509_crl_verify_signature(botan_x509_crl_t crl, botan_pubkey_t key); + BOTAN_FFI_EXPORT(2, 13) int botan_x509_crl_destroy(botan_x509_crl_t crl); /** @@ -2612,7 +2685,7 @@ BOTAN_FFI_EXPORT(3, 11) int botan_x509_crl_entries_count(botan_x509_crl_t crl, s /** * Return the revocation reason code for the given CRL @p entry. -* See RFC 5280 - 5.3.1 for possible reason codes. +* See `botan_x509_crl_reason_code` and RFC 5280 - 5.3.1 for possible reason codes. */ BOTAN_FFI_EXPORT(3, 11) int botan_x509_crl_entry_reason(botan_x509_crl_entry_t entry, int* reason_code); diff --git a/src/lib/ffi/ffi_cert.cpp b/src/lib/ffi/ffi_cert.cpp index 5cd2e26cb01..19a22c16e81 100644 --- a/src/lib/ffi/ffi_cert.cpp +++ b/src/lib/ffi/ffi_cert.cpp @@ -7,7 +7,9 @@ #include #include +#include #include +#include #include #include @@ -142,6 +144,18 @@ int enumerator_count_values(size_t* count, EnumeratorT fn) { } } +std::chrono::system_clock::time_point timepoint_from_timestamp(uint64_t time_since_epoch) { + return std::chrono::system_clock::time_point(std::chrono::seconds(time_since_epoch)); +} + +std::string default_from_ptr(const char* value) { + std::string ret; + if(value != nullptr) { + ret = value; + } + return ret; +} + } // namespace } // namespace Botan_FFI @@ -152,13 +166,6 @@ extern "C" { using namespace Botan_FFI; -#if defined(BOTAN_HAS_X509_CERTIFICATES) - -BOTAN_FFI_DECLARE_STRUCT(botan_x509_cert_struct, Botan::X509_Certificate, 0x8F628937); -BOTAN_FFI_DECLARE_STRUCT(botan_x509_general_name_struct, Botan::GeneralName, 0x563654FD); - -#endif - int botan_x509_cert_load_file(botan_x509_cert_t* cert_obj, const char* cert_path) { if(cert_obj == nullptr || cert_path == nullptr) { return BOTAN_FFI_ERROR_NULL_POINTER; @@ -1028,13 +1035,6 @@ const char* botan_x509_cert_validation_status(int code) { #endif } -#if defined(BOTAN_HAS_X509_CERTIFICATES) - -BOTAN_FFI_DECLARE_STRUCT(botan_x509_crl_struct, Botan::X509_CRL, 0x2C628910); -BOTAN_FFI_DECLARE_STRUCT(botan_x509_crl_entry_struct, Botan::CRL_Entry, 0x4EAA5346); - -#endif - int botan_x509_crl_load_file(botan_x509_crl_t* crl_obj, const char* crl_path) { if(crl_obj == nullptr || crl_path == nullptr) { return BOTAN_FFI_ERROR_NULL_POINTER; @@ -1105,6 +1105,96 @@ int botan_x509_crl_next_update(botan_x509_crl_t crl, uint64_t* time_since_epoch) #endif } +int botan_x509_crl_create(botan_x509_crl_t* crl_obj, + botan_rng_t rng, + botan_x509_cert_t ca_cert, + botan_privkey_t ca_key, + uint64_t issue_time, + uint32_t next_update, + const char* hash_fn, + const char* padding) { + if(Botan::any_null_pointers(crl_obj)) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return ffi_guard_thunk(__func__, [=]() -> int { + auto& rng_ = safe_get(rng); + auto ca = Botan::X509_CA( + safe_get(ca_cert), safe_get(ca_key), default_from_ptr(hash_fn), default_from_ptr(padding), rng_); + auto crl = std::make_unique( + ca.new_crl(rng_, timepoint_from_timestamp(issue_time), std::chrono::seconds(next_update))); + return ffi_new_object(crl_obj, std::move(crl)); + }); +#else + BOTAN_UNUSED(rng, ca_cert, ca_key, hash_fn, padding, issue_time, next_update); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_x509_crl_entry_create(botan_x509_crl_entry_t* entry, botan_x509_cert_t cert, int reason_code) { + if(Botan::any_null_pointers(entry)) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return ffi_guard_thunk(__func__, [=]() -> int { + return ffi_new_object( + entry, std::make_unique(safe_get(cert), static_cast(reason_code))); + }); +#else + BOTAN_UNUSED(cert, reason_code); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_x509_crl_update(botan_x509_crl_t* crl_obj, + botan_x509_crl_t last_crl, + botan_rng_t rng, + botan_x509_cert_t ca_cert, + botan_privkey_t ca_key, + uint64_t issue_time, + uint32_t next_update, + const botan_x509_crl_entry_t* new_entries, + size_t new_entries_len, + const char* hash_fn, + const char* padding) { + if(Botan::any_null_pointers(crl_obj)) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } + if(new_entries_len > 0 && Botan::any_null_pointers(new_entries)) { + return BOTAN_FFI_ERROR_NULL_POINTER; + } +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return ffi_guard_thunk(__func__, [=]() -> int { + auto& rng_ = safe_get(rng); + auto ca = Botan::X509_CA( + safe_get(ca_cert), safe_get(ca_key), default_from_ptr(hash_fn), default_from_ptr(padding), rng_); + + std::vector entries; + entries.reserve(new_entries_len); + for(size_t i = 0; i < new_entries_len; i++) { + entries.push_back(safe_get(new_entries[i])); + } + + auto crl = std::make_unique(ca.update_crl( + safe_get(last_crl), entries, rng_, timepoint_from_timestamp(issue_time), std::chrono::seconds(next_update))); + return ffi_new_object(crl_obj, std::move(crl)); + }); +#else + BOTAN_UNUSED( + last_crl, rng, ca_cert, ca_key, hash_fn, padding, issue_time, next_update, new_entries, new_entries_len); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + +int botan_x509_crl_verify_signature(botan_x509_crl_t crl, botan_pubkey_t key) { +#if defined(BOTAN_HAS_X509_CERTIFICATES) + return BOTAN_FFI_VISIT(crl, [=](const auto& c) -> int { return c.check_signature(safe_get(key)) ? 1 : 0; }); +#else + BOTAN_UNUSED(crl, key); + return BOTAN_FFI_ERROR_NOT_IMPLEMENTED; +#endif +} + int botan_x509_crl_destroy(botan_x509_crl_t crl) { #if defined(BOTAN_HAS_X509_CERTIFICATES) return BOTAN_FFI_CHECKED_DELETE(crl); diff --git a/src/lib/ffi/ffi_cert.h b/src/lib/ffi/ffi_cert.h new file mode 100644 index 00000000000..27bb90e9686 --- /dev/null +++ b/src/lib/ffi/ffi_cert.h @@ -0,0 +1,31 @@ +/* +* (C) 2026 Jack Lloyd +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_FFI_CERT_H_ +#define BOTAN_FFI_CERT_H_ + +#include + +#if defined(BOTAN_HAS_X509_CERTIFICATES) + #include + #include + #include + #include + #include +#endif + +extern "C" { +#if defined(BOTAN_HAS_X509_CERTIFICATES) + +BOTAN_FFI_DECLARE_STRUCT(botan_x509_cert_struct, Botan::X509_Certificate, 0x8F628937); +BOTAN_FFI_DECLARE_STRUCT(botan_x509_crl_struct, Botan::X509_CRL, 0x2C628910); +BOTAN_FFI_DECLARE_STRUCT(botan_x509_crl_entry_struct, Botan::CRL_Entry, 0x4EAA5346); +BOTAN_FFI_DECLARE_STRUCT(botan_x509_general_name_struct, Botan::GeneralName, 0x563654FD); + +#endif +} + +#endif diff --git a/src/lib/ffi/info.txt b/src/lib/ffi/info.txt index 3c5ca9f5f98..d41a692b895 100644 --- a/src/lib/ffi/info.txt +++ b/src/lib/ffi/info.txt @@ -13,6 +13,7 @@ brief -> "C API for Botan's functionality" +ffi_cert.h ffi_ec.h ffi_mp.h ffi_oid.h diff --git a/src/python/botan3.py b/src/python/botan3.py index 6c18a2b77b9..53819774716 100755 --- a/src/python/botan3.py +++ b/src/python/botan3.py @@ -27,6 +27,7 @@ from binascii import hexlify from datetime import datetime from collections.abc import Iterable +from enum import IntEnum # This Python module requires the FFI API version introduced in Botan 3.11.0 # @@ -529,8 +530,23 @@ def ffi_api(fn, args, allowed_errors=None): # X509 CRL ffi_api(dll.botan_x509_crl_load, [c_void_p, c_char_p, c_size_t]) ffi_api(dll.botan_x509_crl_load_file, [c_void_p, c_char_p]) + ffi_api(dll.botan_x509_crl_this_update, [c_void_p, POINTER(c_uint64)]) + ffi_api(dll.botan_x509_crl_next_update, [c_void_p, POINTER(c_uint64)]) + ffi_api(dll.botan_x509_crl_create, + [c_void_p, c_void_p, c_void_p, c_void_p, c_uint64, c_uint32, c_char_p, c_char_p]) + ffi_api(dll.botan_x509_crl_entry_create, [c_void_p, c_void_p, c_int]) + ffi_api(dll.botan_x509_crl_update, + [c_void_p, c_void_p, c_void_p, c_void_p, c_void_p, c_uint64, c_uint32, c_void_p, c_size_t, c_char_p, c_char_p]) + ffi_api(dll.botan_x509_crl_verify_signature, [c_void_p, c_void_p]) ffi_api(dll.botan_x509_crl_destroy, [c_void_p]) ffi_api(dll.botan_x509_is_revoked, [c_void_p, c_void_p], [-1]) + ffi_api(dll.botan_x509_crl_entries, [c_void_p, c_size_t, c_void_p]) + ffi_api(dll.botan_x509_crl_entries_count, [c_void_p, POINTER(c_size_t)]) + ffi_api(dll.botan_x509_crl_entry_reason, [c_void_p, POINTER(c_int)]) + ffi_api(dll.botan_x509_crl_entry_revocation_date, [c_void_p, POINTER(c_uint64)]) + ffi_api(dll.botan_x509_crl_entry_serial_number, [c_void_p, c_void_p]) + ffi_api(dll.botan_x509_crl_entry_view_serial_number, [c_void_p, c_void_p, VIEW_BIN_CALLBACK]) + ffi_api(dll.botan_x509_crl_entry_destroy, [c_void_p]) ffi_api(dll.botan_x509_cert_verify_with_crl, [POINTER(c_int), c_void_p, c_void_p, c_size_t, c_void_p, c_size_t, c_void_p, c_size_t, c_char_p, c_size_t, c_char_p, c_uint64]) @@ -2346,14 +2362,70 @@ def is_revoked(self, crl: X509CRL) -> bool: # # X.509 Certificate revocation lists # + +class X509CRLReason(IntEnum): + UNSPECIFIED = 0 + KEY_COMPROMISE = 1 + CA_COMPROMISE = 2 + AFFILIATION_CHANGED = 3 + SUPERSEDED = 4 + CESSATION_OF_OPERATION = 5 + CERTIFICATE_HOLD = 6 + REMOVE_FROM_CRL = 8 + PRIVILEGE_WITHDRAWN = 9 + AA_COMPROMISE = 10 + + @classmethod + def to_bits(cls, reason: X509CRLReason) -> int: + return reason.value + + @classmethod + def from_bits(cls, reason: int) -> X509CRLReason: + return cls(reason) + + +class X509CRLEntry: + def __init__(self): + self.__obj = c_void_p(0) + + def __del__(self): + _DLL.botan_x509_crl_entry_destroy(self.__obj) + + def handle_(self): + return self.__obj + + @classmethod + def create(cls, cert: X509Cert, reason: X509CRLReason): + entry = X509CRLEntry() + _DLL.botan_x509_crl_entry_create(byref(entry.handle_()), cert.handle_(), X509CRLReason.to_bits(reason)) + return entry + + def serial_number(self) -> MPI: + sn = c_void_p(0) + _DLL.botan_x509_crl_entry_serial_number(self.__obj, byref(sn)) + return MPI(sn) + + def revocation_date(self) -> int: + time = c_uint64(0) + _DLL.botan_x509_crl_entry_revocation_date(self.__obj, byref(time)) + return time.value + + def reason(self) -> X509CRLReason: + reason = c_int(0) + _DLL.botan_x509_crl_entry_reason(self.__obj, byref(reason)) + return X509CRLReason.from_bits(reason.value) + + class X509CRL: """Class representing an X.509 Certificate Revocation List.""" def __init__(self, filename: str | None = None, buf: bytes | None = None): """A CRL in PEM or DER format can be loaded from a file, with the ``filename`` argument, or from a bytestring, with the ``buf`` argument.""" - self.__obj = c_void_p(0) - self.__obj = _load_buf_or_file(filename, buf, _DLL.botan_x509_crl_load_file, _DLL.botan_x509_crl_load) + if not filename and not buf: + self.__obj = c_void_p(0) + else: + self.__obj = _load_buf_or_file(filename, buf, _DLL.botan_x509_crl_load_file, _DLL.botan_x509_crl_load) def __del__(self): _DLL.botan_x509_crl_destroy(self.__obj) @@ -2361,6 +2433,77 @@ def __del__(self): def handle_(self): return self.__obj + @classmethod + def create( + cls, + rng: RandomNumberGenerator, + ca_cert: X509Cert, + ca_key: PrivateKey, + issue_time: int, + next_update: int, + hash_fn: str | None = None, + padding: str | None = None + ) -> X509CRL: + crl = X509CRL() + _DLL.botan_x509_crl_create( + byref(crl.handle_()), + rng.handle_(), + ca_cert.handle_(), + ca_key.handle_(), + issue_time, + next_update, + _ctype_str(hash_fn), + _ctype_str(padding) + ) + return crl + + def revoke( + self, + rng: RandomNumberGenerator, + ca_cert: X509Cert, + ca_key: PrivateKey, + issue_time: int, + next_update: int, + new_entries: list[X509CRLEntry], + hash_fn: str | None = None, + padding: str | None = None + ) -> X509CRL: + crl = X509CRL() + c_revoked = len(new_entries) * c_void_p + arr_new_entries = c_revoked() + for i, entry in enumerate(new_entries): + arr_new_entries[i] = entry.handle_() + new_entries_len = c_size_t(len(new_entries)) + + _DLL.botan_x509_crl_update( + byref(crl.handle_()), + self.__obj, + rng.handle_(), + ca_cert.handle_(), + ca_key.handle_(), + issue_time, + next_update, + arr_new_entries, + new_entries_len, + _ctype_str(hash_fn), + _ctype_str(padding) + ) + return crl + + def revoked(self) -> list[X509CRLEntry]: + count = c_size_t(0) + _DLL.botan_x509_crl_entries_count(self.__obj, byref(count)) + revoked = [] + for i in range(count.value): + entry = X509CRLEntry() + _DLL.botan_x509_crl_entries(self.__obj, c_size_t(i), byref(entry.handle_())) + revoked.append(entry) + return revoked + + def verify(self, key: PublicKey) -> bool: + rc = _DLL.botan_x509_crl_verify_signature(self.__obj, key.handle_()) + return rc == 1 + class MPI: """Most of the usual arithmetic operators (``__add__``, ``__mul__``, etc) are defined.""" diff --git a/src/scripts/test_python.py b/src/scripts/test_python.py index 3d63cb4fd68..a17352385ef 100644 --- a/src/scripts/test_python.py +++ b/src/scripts/test_python.py @@ -12,6 +12,7 @@ import platform import argparse import sys +import time from itertools import permutations # Starting with Python 3.8 DLL search locations are more restricted on Windows. @@ -949,6 +950,40 @@ def test_certs(self): self.assertFalse(int04_1.is_revoked(rootcrl)) self.assertTrue(end21.is_revoked(int21crl)) + def test_crls(self): + rng = botan.RandomNumberGenerator() + now = int(time.time()) + + priv_pem = """ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgoVEKnWZw2Bfrf3MM +WLrfvRcAqq/sOf58jny37NLGQHShRANCAARageRLkKQEh1M86zvqeeesx2u9duLP +iWtHjIcunpiq6+IiB8IVu7Ncu6uPKoFS/mWzTvjgdNusmgNle9p3OAbE +-----END PRIVATE KEY-----""" + + ca_cert = botan.X509Cert(filename=test_data("src/tests/data/x509/crl/ca.crt")) + ca_key = botan.PrivateKey.load(priv_pem) + ca_pubkey = ca_key.get_public_key() + + sub1_cert = botan.X509Cert(filename=test_data("src/tests/data/x509/crl/sub1.crt")) + sub2_cert = botan.X509Cert(filename=test_data("src/tests/data/x509/crl/sub2.crt")) + + crl = botan.X509CRL.create(rng, ca_cert, ca_key, now, 600) + self.assertTrue(crl.verify(ca_pubkey)) + self.assertEqual(sub1_cert.verify(None, [ca_cert], crls=[crl]), 0) + self.assertEqual(sub2_cert.verify(None, [ca_cert], crls=[crl]), 0) + + to_revoke = botan.X509CRLEntry.create(sub2_cert, botan.X509CRLReason.KEY_COMPROMISE) + + crl = crl.revoke(rng, ca_cert, ca_key, now, 86400, [to_revoke]) + self.assertTrue(crl.verify(ca_pubkey)) + self.assertEqual(sub1_cert.verify(None, [ca_cert], crls=[crl]), 0) + self.assertEqual(sub2_cert.verify(None, [ca_cert], crls=[crl]), 5000) + self.assertEqual(len(crl.revoked()), 1) + revoked_entry = crl.revoked()[0] + self.assertEqual(revoked_entry.reason(), botan.X509CRLReason.KEY_COMPROMISE) + self.assertEqual(revoked_entry.serial_number(), botan.MPI("270431672985589325219914342203841486494")) + self.assertTrue(now - 20 <= revoked_entry.revocation_date() <= now + 20) def test_mpi(self): z = botan.MPI() diff --git a/src/tests/data/x509/crl/ca.crt b/src/tests/data/x509/crl/ca.crt new file mode 100644 index 00000000000..0fb3decb480 --- /dev/null +++ b/src/tests/data/x509/crl/ca.crt @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBxTCCAWugAwIBAgIRANaPVuZZ0F1TP1Cyepqi9UcwCgYIKoZIzj0EAwIwKzEp +MCcGA1UEAxMgVGVzdCBDQS9VUy9Cb3RhbiBQcm9qZWN0L1Rlc3RpbmcwIBcNMjUx +MTIxMTUzOTQ2WhgPMjEyNTEwMjgxNTM5NDZaMCsxKTAnBgNVBAMTIFRlc3QgQ0Ev +VVMvQm90YW4gUHJvamVjdC9UZXN0aW5nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD +QgAEWoHkS5CkBIdTPOs76nnnrMdrvXbiz4lrR4yHLp6YquviIgfCFbuzXLurjyqB +Uv5ls0744HTbrJoDZXvadzgGxKNuMGwwIQYDVR0OBBoEGM6/Sxg0CYRYRGfOWslf +ejh8frTqyJmK9jAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBATAj +BgNVHSMEHDAagBjOv0sYNAmEWERnzlrJX3o4fH606siZivYwCgYIKoZIzj0EAwID +SAAwRQIgB1pUBb+jznOrFBTDz9r60f6Q548KdqQX5IEALLD+Gl0CIQCCh6ZvmIGk +Poyp/IDllrZcbKGbMPvMHE81r33DwgShBg== +-----END CERTIFICATE----- diff --git a/src/tests/data/x509/crl/sub1.crt b/src/tests/data/x509/crl/sub1.crt new file mode 100644 index 00000000000..6443dc91f5f --- /dev/null +++ b/src/tests/data/x509/crl/sub1.crt @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBtjCCAVygAwIBAgIRANYzqdzaS1B3OIKXqOo2NK0wCgYIKoZIzj0EAwIwKzEp +MCcGA1UEAxMgVGVzdCBDQS9VUy9Cb3RhbiBQcm9qZWN0L1Rlc3RpbmcwIBcNMjUx +MTIxMTU0MjE0WhgPMjEyNTEwMjgxNTQyMTRaMDIxMDAuBgNVBAMTJ1Rlc3QgQ2Vy +dCBTdWIxL1VTL0JvdGFuIFByb2plY3QvVGVzdGluZzBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABPuKRaa3tieYoeaIq0EwKgEaWQQqArGEJI4voYKoyz4Yr4PoAowX +Aw6iay0vSDcu2L3q6RsNlS0kR1COb+qfh22jWDBWMCEGA1UdDgQaBBg8yOoSIn4A +jnsn42OMNUCjcQ/skT6rbQ4wDAYDVR0TAQH/BAIwADAjBgNVHSMEHDAagBjOv0sY +NAmEWERnzlrJX3o4fH606siZivYwCgYIKoZIzj0EAwIDSAAwRQIhAJ3K4x2Jlgvu +n6p9N+7O4X+auqbkeTBrvDWymJBOcoCuAiA6KO5WedzThA4c+seatfc3Lr8WLM9f +CxcfHT040ib+lQ== +-----END CERTIFICATE----- diff --git a/src/tests/data/x509/crl/sub2.crt b/src/tests/data/x509/crl/sub2.crt new file mode 100644 index 00000000000..7c8b53e1b5f --- /dev/null +++ b/src/tests/data/x509/crl/sub2.crt @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBtjCCAVygAwIBAgIRAMtzPuWhJ9gzszvF+b/Knp4wCgYIKoZIzj0EAwIwKzEp +MCcGA1UEAxMgVGVzdCBDQS9VUy9Cb3RhbiBQcm9qZWN0L1Rlc3RpbmcwIBcNMjUx +MTIxMTU0MjE4WhgPMjEyNTEwMjgxNTQyMThaMDIxMDAuBgNVBAMTJ1Rlc3QgQ2Vy +dCBTdWIyL1VTL0JvdGFuIFByb2plY3QvVGVzdGluZzBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABJOWUmHvX7EEIcpOeYOd4olY5/FhK8R81zrEAj7ZFZzdUPkSWQo7 +Tw4wEKhEh5yWnpf5/GYPeLeJVILNl6V3CC2jWDBWMCEGA1UdDgQaBBjVnj0PMoah +Z75YsOESDdjAOKRH2X00f1wwDAYDVR0TAQH/BAIwADAjBgNVHSMEHDAagBjOv0sY +NAmEWERnzlrJX3o4fH606siZivYwCgYIKoZIzj0EAwIDSAAwRQIgbsswTSNN5oiY +RjfYKoLqBqiSXUwMobCm/a/AmYWCm1gCIQCT5AS0BZYHqcJaMFnvqvQoiP5pG4UA +rWSdD+Aor4KcEQ== +-----END CERTIFICATE----- diff --git a/src/tests/test_ffi.cpp b/src/tests/test_ffi.cpp index 00e0ac2d56b..bf60eec7df6 100644 --- a/src/tests/test_ffi.cpp +++ b/src/tests/test_ffi.cpp @@ -678,7 +678,7 @@ class FFI_CRL_Test final : public FFI_Test { public: std::string name() const override { return "FFI CRL"; } - void ffi_test(Test::Result& result, botan_rng_t /*unused*/) override { + void ffi_test(Test::Result& result, botan_rng_t rng) override { const char* crl_string = "-----BEGIN X509 CRL-----\n" "MIICoTCCAQkCAQEwDQYJKoZIhvcNAQELBQAwgZQxLTArBgNVBAMTJFVzYWJsZSBj\n" @@ -817,6 +817,104 @@ class FFI_CRL_Test final : public FFI_Test { TEST_FFI_OK(botan_x509_crl_destroy, (crl)); TEST_FFI_OK(botan_x509_crl_destroy, (bytecrl)); TEST_FFI_OK(botan_x509_crl_destroy, (crl_without_next_update)); + + const uint64_t now = + std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()) + .count(); + + const char* priv_string = + "-----BEGIN PRIVATE KEY-----\n" + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgoVEKnWZw2Bfrf3MM\n" + "WLrfvRcAqq/sOf58jny37NLGQHShRANCAARageRLkKQEh1M86zvqeeesx2u9duLP\n" + "iWtHjIcunpiq6+IiB8IVu7Ncu6uPKoFS/mWzTvjgdNusmgNle9p3OAbE\n" + "-----END PRIVATE KEY-----"; + + botan_privkey_t ca_key; + botan_x509_cert_t ca_cert; + botan_x509_cert_t sub1_cert; + botan_x509_cert_t sub2_cert; + + REQUIRE_FFI_OK(botan_privkey_load, + (&ca_key, nullptr, reinterpret_cast(priv_string), 240, nullptr)); + REQUIRE_FFI_OK(botan_x509_cert_load_file, (&ca_cert, Test::data_file("x509/crl/ca.crt").c_str())); + REQUIRE_FFI_OK(botan_x509_cert_load_file, (&sub1_cert, Test::data_file("x509/crl/sub1.crt").c_str())); + REQUIRE_FFI_OK(botan_x509_cert_load_file, (&sub2_cert, Test::data_file("x509/crl/sub2.crt").c_str())); + + botan_pubkey_t ca_pubkey; + REQUIRE_FFI_OK(botan_privkey_export_pubkey, (&ca_pubkey, ca_key)); + + botan_x509_crl_t empty_crl; + TEST_FFI_OK(botan_x509_crl_create, (&empty_crl, rng, ca_cert, ca_key, now, 86400, nullptr, nullptr)); + + int rc; + // both validate, because the crl is empty + TEST_FFI_RC(0, + botan_x509_cert_verify_with_crl, + (&rc, sub1_cert, nullptr, 0, &ca_cert, 1, &empty_crl, 1, nullptr, 0, nullptr, 0)); + TEST_FFI_RC(0, + botan_x509_cert_verify_with_crl, + (&rc, sub2_cert, nullptr, 0, &ca_cert, 1, &empty_crl, 1, nullptr, 0, nullptr, 0)); + + botan_x509_crl_entry_t crl_entry; + TEST_FFI_RC(BOTAN_FFI_ERROR_OUT_OF_RANGE, botan_x509_crl_entries, (empty_crl, 0, &crl_entry)); + TEST_FFI_OK(botan_x509_crl_entry_create, (&crl_entry, sub2_cert, BOTAN_CRL_ENTRY_KEY_COMPROMISE)); + + botan_x509_crl_t new_crl; + botan_x509_crl_entry_t crl_entries[1] = {crl_entry}; + + TEST_FFI_RC(BOTAN_FFI_ERROR_NULL_POINTER, + botan_x509_crl_update, + (&new_crl, empty_crl, rng, ca_cert, ca_key, now, 86400, nullptr, 1, nullptr, nullptr)); + TEST_FFI_OK(botan_x509_crl_update, + (&new_crl, empty_crl, rng, ca_cert, ca_key, now, 86400, crl_entries, 1, nullptr, nullptr)); + // sub 1 still validates + TEST_FFI_RC(0, + botan_x509_cert_verify_with_crl, + (&rc, sub1_cert, nullptr, 0, &ca_cert, 1, &new_crl, 1, nullptr, 0, nullptr, 0)); + // but sub 2 is revoked + TEST_FFI_RC(1, + botan_x509_cert_verify_with_crl, + (&rc, sub2_cert, nullptr, 0, &ca_cert, 1, &new_crl, 1, nullptr, 0, nullptr, 0)); + + botan_x509_crl_entry_t crl_entry_2; + TEST_FFI_RC(BOTAN_FFI_ERROR_OUT_OF_RANGE, botan_x509_crl_entries, (new_crl, 1, &crl_entry_2)); + TEST_FFI_OK(botan_x509_crl_entries, (new_crl, 0, &crl_entry_2)); + + botan_mp_t serial_from_str; + TEST_FFI_OK(botan_mp_init, (&serial_from_str)); + TEST_FFI_OK(botan_mp_set_from_str, (serial_from_str, "270431672985589325219914342203841486494")); + + uint64_t expire_time; + TEST_FFI_OK(botan_x509_crl_entry_revocation_date, (crl_entry_2, &expire_time)); + TEST_FFI_OK(botan_x509_crl_entry_reason, (crl_entry_2, &reason)); + + botan_mp_t serial_from_crl; + TEST_FFI_OK(botan_x509_crl_entry_serial_number, (crl_entry_2, &serial_from_crl)); + TEST_FFI_RC(1, botan_mp_equal, (serial_from_str, serial_from_crl)); + result.test_is_true("expire time is correct", now - 20 <= expire_time && expire_time <= now + 20); + result.test_is_true("reason is correct", reason == BOTAN_CRL_ENTRY_KEY_COMPROMISE); + + TEST_FFI_RC(1, botan_x509_crl_verify_signature, (new_crl, ca_pubkey)); + + botan_x509_crl_t even_newer_crl; + TEST_FFI_OK(botan_x509_crl_update, + (&even_newer_crl, new_crl, rng, ca_cert, ca_key, now, 456, nullptr, 0, nullptr, nullptr)); + + TEST_FFI_OK(botan_x509_crl_next_update, (even_newer_crl, &expire_time)); + result.test_is_true("expire time is correct", expire_time == now + 456); + + TEST_FFI_OK(botan_x509_crl_entry_destroy, (crl_entry)); + TEST_FFI_OK(botan_x509_crl_entry_destroy, (crl_entry_2)); + TEST_FFI_OK(botan_mp_destroy, (serial_from_str)); + TEST_FFI_OK(botan_mp_destroy, (serial_from_crl)); + TEST_FFI_OK(botan_x509_crl_destroy, (empty_crl)); + TEST_FFI_OK(botan_x509_crl_destroy, (new_crl)); + TEST_FFI_OK(botan_x509_crl_destroy, (even_newer_crl)); + TEST_FFI_OK(botan_x509_cert_destroy, (ca_cert)); + TEST_FFI_OK(botan_x509_cert_destroy, (sub1_cert)); + TEST_FFI_OK(botan_x509_cert_destroy, (sub2_cert)); + TEST_FFI_OK(botan_pubkey_destroy, (ca_pubkey)); + TEST_FFI_OK(botan_privkey_destroy, (ca_key)); } };