Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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*.
Expand Down
119 changes: 119 additions & 0 deletions src/cli/x509.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@
*/

#include "cli.h"
#include <algorithm>
#include <memory>

#if defined(BOTAN_HAS_X509_CERTIFICATES) && defined(BOTAN_TARGET_OS_HAS_FILESYSTEM)

#include <botan/certstor.h>
#include <botan/data_src.h>
#include <botan/pk_keys.h>
#include <botan/pkcs8.h>
#include <botan/pkix_enums.h>
#include <botan/pkix_types.h>
#include <botan/x509_ca.h>
#include <botan/x509_ext.h>
#include <botan/x509_key.h>
#include <botan/x509cert.h>
#include <botan/x509path.h>
#include <botan/x509self.h>
Expand Down Expand Up @@ -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<Botan::Public_Key> 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<Botan::Cert_Extension::Basic_Constraints>(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<Botan::Cert_Extension::Authority_Key_ID>(ca_cert.subject_key_id()));

extensions.replace(std::make_unique<Botan::Cert_Extension::Subject_Key_ID>(
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<int, std::ratio<86400>> 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<Botan::Cert_Extension::Subject_Alternative_Name> 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<Botan::Cert_Extension::Subject_Alternative_Name>(subject_alt);
}

void add_extended_key_usage_extension(Botan::Extensions& extensions) {
std::vector<Botan::OID> 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<Botan::Cert_Extension::Extended_Key_Usage>(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<Botan::Cert_Extension::Key_Usage>(constraints));
}
}
};

BOTAN_REGISTER_COMMAND("create_cert_for_key", Create_Cert_for_Key);

} // namespace

} // namespace Botan_CLI
Expand Down
13 changes: 10 additions & 3 deletions src/scripts/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading