diff --git a/doc/cli.rst b/doc/cli.rst index 0241753f3b6..11fd787a16f 100644 --- a/doc/cli.rst +++ b/doc/cli.rst @@ -202,6 +202,13 @@ X.509 defaults to the padding scheme used in the CA certificate, or otherwise some suitable default. +``create_cert_for_key client_public_key ca_cert ca_key CN --ca-key-pass= --duration=365 --country= --organization= --ca --path-limit=1 --email= --dns= --ext-ku= --hash= --padding="`` + Create a certificate directly for the *client_public_key* using *ca_key* and *ca_cert* of + the desired CA. This is necessary to issue certificates for KEM keys, which cannot be used to sign PKCS#10 requests. + If the CA's private key is encrypted, the decryption passphrase has to be provided with *--ca-key-pass=...*. + The client public key file can be created from the private key using the above described ``pkcs8`` + command with the flag ``--pub-out``. + ``ocsp_check --timeout=3000 subject issuer`` Verify an X.509 certificate against the issuers OCSP responder. Pass the certificate to validate as *subject* and the CA certificate as *issuer*. diff --git a/src/cli/x509.cpp b/src/cli/x509.cpp index 6162ca0188b..fca7542a705 100644 --- a/src/cli/x509.cpp +++ b/src/cli/x509.cpp @@ -6,6 +6,8 @@ */ #include "cli.h" +#include +#include #if defined(BOTAN_HAS_X509_CERTIFICATES) && defined(BOTAN_TARGET_OS_HAS_FILESYSTEM) @@ -13,7 +15,11 @@ #include #include #include + #include + #include #include + #include + #include #include #include #include @@ -381,6 +387,119 @@ class Generate_PKCS10 final : public Command { BOTAN_REGISTER_COMMAND("gen_pkcs10", Generate_PKCS10); +class Create_Cert_for_Key final : public Command { + public: + Create_Cert_for_Key() : + Command( + "create_cert_for_key client_public_key ca_cert ca_key CN --ca-key-pass= --duration=365 --country= --organization= " + "--ca --path-limit=1 --email= --dns= --ext-ku= --hash= --padding=") {} + + std::string group() const override { return "x509"; } + + std::string description() const override { + return "Generate a certificate by a CA for a given public key. This is for instance needed to issue ML-KEM certificates, for which PKCS#10 requests cannot be generated."; + } + + void go() override { + const std::string ca_key_file = get_arg("ca_key"); + const std::string passphrase = get_passphrase_arg("Passphrase for CA key " + ca_key_file, "ca-key-pass"); + + auto key = load_private_key(ca_key_file, passphrase); + std::unique_ptr client_pub_key = Botan::X509::load_key(get_arg("client_public_key")); + + const Botan::X509_Certificate ca_cert(get_arg("ca_cert")); + + const std::string hash = get_arg("hash"); + + size_t path_limit = 0; + const bool is_ca = flag_set("ca"); + if(is_ca) { + path_limit = get_arg_sz("path-limit"); + } + + const std::string padding = get_arg("padding"); + Botan::X509_CA ca(ca_cert, *key, hash, padding, rng()); + + Botan::Extensions extensions; + extensions.add_new(std::make_unique(is_ca, path_limit)); + extensions.replace(create_alt_name_ext()); + + add_key_usage_extension(extensions, *client_pub_key, is_ca); + + add_extended_key_usage_extension(extensions); + + extensions.replace(std::make_unique(ca_cert.subject_key_id())); + + extensions.replace(std::make_unique( + client_pub_key->subject_public_key(), ca.hash_function())); + + Botan::X509_DN subject_dn; + + subject_dn.add_attribute("X520.CommonName", get_arg("CN")); + subject_dn.add_attribute("X520.Country", get_arg("country")); + subject_dn.add_attribute("X520.Organization", get_arg("organization")); + + auto now = std::chrono::system_clock::now(); + + const Botan::X509_Time start_time(now); + + typedef std::chrono::duration> days; + + const Botan::X509_Time end_time(now + days(get_arg_sz("duration"))); + + const Botan::X509_Certificate new_cert = Botan::X509_CA::make_cert(ca.signature_op(), + rng(), + ca.algorithm_identifier(), + client_pub_key->subject_public_key(), + start_time, + end_time, + ca_cert.subject_dn(), + subject_dn, + extensions); + update_stateful_private_key(*key, rng(), ca_key_file, passphrase); + + output() << new_cert.PEM_encode(); + } + + private: + std::unique_ptr create_alt_name_ext() { + Botan::AlternativeName subject_alt; + + auto more_dns = Command::split_on(get_arg("dns"), ','); + for(const auto& nm : more_dns) { + subject_alt.add_dns(nm); + } + subject_alt.add_email(get_arg("email")); + + return std::make_unique(subject_alt); + } + + void add_extended_key_usage_extension(Botan::Extensions& extensions) { + std::vector ex_constraints; + for(const std::string& ext_ku : Command::split_on(get_arg("ext-ku"), ',')) { + ex_constraints.push_back(Botan::OID(ext_ku)); + } + if(!ex_constraints.empty()) { + extensions.add_new(std::make_unique(ex_constraints)); + } + } + + void add_key_usage_extension(Botan::Extensions& extensions, const Botan::Public_Key& client_pub_key, bool is_ca) { + const Botan::Key_Constraints constraints = + is_ca ? Botan::Key_Constraints::ca_constraints() : Botan::Key_Constraints(); + + if(!constraints.compatible_with(client_pub_key)) { + throw Botan::Invalid_Argument("The requested key constraints are incompatible with the algorithm"); + } + + if(!constraints.empty()) { + extensions.add_new(std::make_unique(constraints)); + } + } +}; + +BOTAN_REGISTER_COMMAND("create_cert_for_key", Create_Cert_for_Key); + } // namespace } // namespace Botan_CLI diff --git a/src/scripts/test_cli.py b/src/scripts/test_cli.py index 1709c0f3dc0..eb4e02ed17d 100755 --- a/src/scripts/test_cli.py +++ b/src/scripts/test_cli.py @@ -1003,15 +1003,22 @@ def cli_cert_issuance_tests(tmp_dir, algos=None): test_cli("gen_pkcs10", "%s Intermediate --ca --output=%s" % (int_key, int_csr)) test_cli("sign_cert", "%s %s %s --output=%s" % (root_crt, root_key, int_csr, int_crt)) - test_cli("gen_pkcs10", "%s Leaf --output=%s" % (leaf_key, leaf_csr)) - test_cli("sign_cert", "%s %s %s --output=%s" % (int_crt, int_key, leaf_csr, leaf_crt)) + if algos[2][0] != "ML-KEM": + test_cli("gen_pkcs10", "%s Leaf --output=%s" % (leaf_key, leaf_csr)) + test_cli("sign_cert", "%s %s %s --output=%s" % (int_crt, int_key, leaf_csr, leaf_crt)) + else: + leaf_pub = os.path.join(tmp_dir, 'leaf.pub') + test_cli("pkcs8", "%s --pub-out --output=%s" % (leaf_key, leaf_pub )) + test_cli("create_cert_for_key", "%s %s %s Leaf --output=%s" % (leaf_pub, int_crt, int_key, leaf_crt)) test_cli("cert_verify", "%s %s %s" % (leaf_crt, int_crt, root_crt), "Certificate passes validation checks") def cli_cert_issuance_alternative_algos_tests(tmp_dir): for i, algo in enumerate([[("Dilithium", "Dilithium-8x7-AES-r3"), ("Dilithium", "Dilithium-8x7-AES-r3"), ("Dilithium", "Dilithium-8x7-AES-r3")], [("ECDSA", "secp256r1"), ("ECDSA", "secp384r1"), ("ECDSA", "secp256r1")], - [("Dilithium", "Dilithium-6x5-r3"), ("ECDSA", "secp256r1"), ("RSA", "2048")]]): + [("Dilithium", "Dilithium-6x5-r3"), ("ECDSA", "secp256r1"), ("RSA", "2048")], + [("Dilithium", "Dilithium-6x5-r3"), ("ECDSA", "secp256r1"), ("ML-KEM", "")] + ]): sub_tmp_dir = os.path.join(tmp_dir, str(i)) os.mkdir(sub_tmp_dir) cli_cert_issuance_tests(sub_tmp_dir, algo)