diff --git a/doc/11-cli-commands.md b/doc/11-cli-commands.md index 7eac4247c..0e7175b36 100644 --- a/doc/11-cli-commands.md +++ b/doc/11-cli-commands.md @@ -38,6 +38,7 @@ Supported commands: * pki save-cert (saves another Icinga 2 instance's certificate) * pki sign-csr (signs a CSR) * pki ticket (generates a ticket) + * pki verify (verify TLS certificates: CN, signed by CA, is CA; Print certificate) * variable get (gets a variable) * variable list (lists all variables) @@ -570,7 +571,7 @@ You will need them in the [distributed monitoring chapter](06-distributed-monito ``` # icinga2 pki --help -icinga2 - The Icinga 2 network monitoring daemon (version: v2.11.0) +icinga2 - The Icinga 2 network monitoring daemon (version: v2.12.0) Usage: icinga2 [] @@ -582,6 +583,7 @@ Supported commands: * pki save-cert (saves another Icinga 2 instance's certificate) * pki sign-csr (signs a CSR) * pki ticket (generates a ticket) + * pki verify (verify TLS certificates: CN, signed by CA, is CA; Print certificate) Global options: -h [ --help ] show this help message diff --git a/doc/15-troubleshooting.md b/doc/15-troubleshooting.md index 19d9f85d4..fdb29570f 100644 --- a/doc/15-troubleshooting.md +++ b/doc/15-troubleshooting.md @@ -1032,94 +1032,179 @@ Print the CA and client certificate and ensure that the following attributes are * v3 extensions must set the basic constraint for `CA:TRUE` (ca.crt) or `CA:FALSE` (client certificate). * Subject Alternative Name is set to the resolvable DNS name (required for REST API and browsers). - Navigate into the local certificate store: ``` $ cd /var/lib/icinga2/certs/ ``` -Print the CA certificate: +Make sure to verify the agents' certificate and its stored `ca.crt` in `/var/lib/icinga2/certs` and ensure that +all instances (master, satellite, agent) are signed by the **same CA**. + +Compare the `ca.crt` file from the agent node and compare it to your master's `ca.crt` file. + + +Since 2.12, you can use the built-in CLI command `pki verify` to perform TLS certificate validation tasks. + +> **Hint** +> +> The CLI command uses exit codes aligned to the [Plugin API specification](05-service-monitoring.md#service-monitoring-plugin-api). +> Run the commands followed with `echo $?` to see the exit code. + +These CLI commands can be used on Windows agents too without requiring the OpenSSL binary. + +#### Print TLS Certificate + +Pass the certificate file to the `--cert` CLI command parameter to print its details. +This prints a shorter version of `openssl x509 -in -text`. ``` -$ openssl x509 -in ca.crt -text +$ icinga2 pki verify --cert icinga2-agent2.localdomain.crt -Certificate: - Data: - Version: 3 (0x2) - Serial Number: 1 (0x1) - Signature Algorithm: sha256WithRSAEncryption - Issuer: CN=Icinga CA - Validity - Not Before: Feb 23 14:45:32 2016 GMT - Not After : Feb 19 14:45:32 2031 GMT - Subject: CN=Icinga CA - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - Public-Key: (4096 bit) - Modulus: -... - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Basic Constraints: critical - CA:TRUE - Signature Algorithm: sha256WithRSAEncryption -... +information/cli: Printing certificate 'icinga2-agent2.localdomain.crt' + + Version: 3 + Subject: CN = icinga2-agent2.localdomain + Issuer: CN = Icinga CA + Valid From: Feb 14 11:29:36 2020 GMT + Valid Until: Feb 10 11:29:36 2035 GMT + Serial: 12:fe:a6:22:f5:e3:db:a2:95:8e:92:b2:af:1a:e3:01:44:c4:70:e0 + + Signature Algorithm: sha256WithRSAEncryption + Subject Alt Names: icinga2-agent2.localdomain + Fingerprint: 40 98 A0 77 58 4F CA D1 05 AC 18 53 D7 52 8D D7 9C 7F 5A 23 B4 AF 63 A4 92 9D DC FF 89 EF F1 4C ``` -Print the client public certificate: +You can also print the `ca.crt` certificate without any further checks using the `--cert` parameter. + +#### Print and Verify CA Certificate + +The `--cacert` CLI parameter allows to check whether the given certificate file is a public CA certificate. ``` -$ openssl x509 -in icinga2-agent1.localdomain.crt -text +$ icinga2 pki verify --cacert ca.crt -Certificate: - Data: - Version: 3 (0x2) - Serial Number: - 86:47:44:65:49:c6:65:6b:5e:6d:4f:a5:fe:6c:76:05:0b:1a:cf:34 - Signature Algorithm: sha256WithRSAEncryption - Issuer: CN=Icinga CA - Validity - Not Before: Aug 20 16:20:05 2016 GMT - Not After : Aug 17 16:20:05 2031 GMT - Subject: CN=icinga2-agent1.localdomain - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - Public-Key: (4096 bit) - Modulus: -... - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Basic Constraints: critical - CA:FALSE - X509v3 Subject Alternative Name: - DNS:icinga2-agent1.localdomain - Signature Algorithm: sha256WithRSAEncryption -... +information/cli: Checking whether certificate 'ca.crt' is a valid CA certificate. + + Version: 3 + Subject: CN = Icinga CA + Issuer: CN = Icinga CA + Valid From: Jul 31 12:26:08 2019 GMT + Valid Until: Jul 27 12:26:08 2034 GMT + Serial: 89:fe:d6:12:66:25:3a:c5:07:c1:eb:d4:e6:f2:df:ca:13:6e:dc:e7 + + Signature Algorithm: sha256WithRSAEncryption + Subject Alt Names: + Fingerprint: 9A 11 29 A8 A3 89 F8 56 30 1A E4 0A B2 6B 28 46 07 F0 14 17 BD 19 A4 FC BD 41 40 B5 1A 8F BF 20 + +information/cli: OK: CA certificate file 'ca.crt' was verified successfully. ``` -Make sure to verify the client's certificate and its received `ca.crt` in `/var/lib/icinga2/certs` and ensure that -both instances are signed by the **same CA**. +In case you pass a wrong certificate, an error is shown and the exit code is `2` (Critical). ``` -$ openssl verify -verbose -CAfile /var/lib/icinga2/certs/ca.crt /var/lib/icinga2/certs/icinga2-master1.localdomain.crt +$ icinga2 pki verify --cacert icinga2-agent2.localdomain.crt -icinga2-master1.localdomain.crt: OK +information/cli: Checking whether certificate 'icinga2-agent2.localdomain.crt' is a valid CA certificate. + + Version: 3 + Subject: CN = icinga2-agent2.localdomain + Issuer: CN = Icinga CA + Valid From: Feb 14 11:29:36 2020 GMT + Valid Until: Feb 10 11:29:36 2035 GMT + Serial: 12:fe:a6:22:f5:e3:db:a2:95:8e:92:b2:af:1a:e3:01:44:c4:70:e0 + + Signature Algorithm: sha256WithRSAEncryption + Subject Alt Names: icinga2-agent2.localdomain + Fingerprint: 40 98 A0 77 58 4F CA D1 05 AC 18 53 D7 52 8D D7 9C 7F 5A 23 B4 AF 63 A4 92 9D DC FF 89 EF F1 4C + +critical/cli: CRITICAL: The file 'icinga2-agent2.localdomain.crt' does not seem to be a CA certificate file. ``` -``` -$ openssl verify -verbose -CAfile /var/lib/icinga2/certs/ca.crt /var/lib/icinga2/certs/icinga2-agent1.localdomain.crt +#### Verify Certificate is signed by CA Certificate -icinga2-agent1.localdomain.crt: OK -``` - -Fetch the `ca.crt` file from the client node and compare it to your master's `ca.crt` file: +Pass the certificate file to the `--cert` CLI parameter, and the `ca.crt` file to the `--cacert` parameter. +Common troubleshooting scenarios involve self-signed certificates and untrusted agents resulting in disconnects. ``` -$ scp icinga2-agent1:/var/lib/icinga2/certs/ca.crt test-client-ca.crt -$ diff -ur /var/lib/icinga2/certs/ca.crt test-client-ca.crt +$ icinga2 pki verify --cert icinga2-agent2.localdomain.crt --cacert ca.crt + +information/cli: Verifying certificate 'icinga2-agent2.localdomain.crt' + + Version: 3 + Subject: CN = icinga2-agent2.localdomain + Issuer: CN = Icinga CA + Valid From: Feb 14 11:29:36 2020 GMT + Valid Until: Feb 10 11:29:36 2035 GMT + Serial: 12:fe:a6:22:f5:e3:db:a2:95:8e:92:b2:af:1a:e3:01:44:c4:70:e0 + + Signature Algorithm: sha256WithRSAEncryption + Subject Alt Names: icinga2-agent2.localdomain + Fingerprint: 40 98 A0 77 58 4F CA D1 05 AC 18 53 D7 52 8D D7 9C 7F 5A 23 B4 AF 63 A4 92 9D DC FF 89 EF F1 4C + +information/cli: with CA certificate 'ca.crt'. + + Version: 3 + Subject: CN = Icinga CA + Issuer: CN = Icinga CA + Valid From: Jul 31 12:26:08 2019 GMT + Valid Until: Jul 27 12:26:08 2034 GMT + Serial: 89:fe:d6:12:66:25:3a:c5:07:c1:eb:d4:e6:f2:df:ca:13:6e:dc:e7 + + Signature Algorithm: sha256WithRSAEncryption + Subject Alt Names: + Fingerprint: 9A 11 29 A8 A3 89 F8 56 30 1A E4 0A B2 6B 28 46 07 F0 14 17 BD 19 A4 FC BD 41 40 B5 1A 8F BF 20 + +information/cli: OK: Certificate with CN 'icinga2-agent2.localdomain' is signed by CA. ``` +#### Verify Certificate matches Common Name (CN) + +This allows to verify the common name inside the certificate with a given string parameter. +Typical troubleshooting involve upper/lower case CNs (Windows). + +``` +$ icinga2 pki verify --cert icinga2-agent2.localdomain.crt --cn icinga2-agent2.localdomain + +information/cli: Verifying common name (CN) 'icinga2-agent2.localdomain in certificate 'icinga2-agent2.localdomain.crt'. + + Version: 3 + Subject: CN = icinga2-agent2.localdomain + Issuer: CN = Icinga CA + Valid From: Feb 14 11:29:36 2020 GMT + Valid Until: Feb 10 11:29:36 2035 GMT + Serial: 12:fe:a6:22:f5:e3:db:a2:95:8e:92:b2:af:1a:e3:01:44:c4:70:e0 + + Signature Algorithm: sha256WithRSAEncryption + Subject Alt Names: icinga2-agent2.localdomain + Fingerprint: 40 98 A0 77 58 4F CA D1 05 AC 18 53 D7 52 8D D7 9C 7F 5A 23 B4 AF 63 A4 92 9D DC FF 89 EF F1 4C + +information/cli: OK: CN 'icinga2-agent2.localdomain' matches certificate CN 'icinga2-agent2.localdomain'. +``` + +In the example below, the certificate uses an upper case CN. + +``` +$ icinga2 pki verify --cert icinga2-agent2.localdomain.crt --cn icinga2-agent2.localdomain + +information/cli: Verifying common name (CN) 'icinga2-agent2.localdomain in certificate 'icinga2-agent2.localdomain.crt'. + + Version: 3 + Subject: CN = ICINGA2-agent2.localdomain + Issuer: CN = Icinga CA + Valid From: Feb 14 11:29:36 2020 GMT + Valid Until: Feb 10 11:29:36 2035 GMT + Serial: 12:fe:a6:22:f5:e3:db:a2:95:8e:92:b2:af:1a:e3:01:44:c4:70:e0 + + Signature Algorithm: sha256WithRSAEncryption + Subject Alt Names: ICINGA2-agent2.localdomain + Fingerprint: 40 98 A0 77 58 4F CA D1 05 AC 18 53 D7 52 8D D7 9C 7F 5A 23 B4 AF 63 A4 92 9D DC FF 89 EF F1 4C + +critical/cli: CRITICAL: CN 'icinga2-agent2.localdomain' does NOT match certificate CN 'icinga2-agent2.localdomain'. +``` + + + ### Certificate Signing Icinga offers two methods: diff --git a/doc/16-upgrading-icinga-2.md b/doc/16-upgrading-icinga-2.md index 7d18fed58..1a6cb338d 100644 --- a/doc/16-upgrading-icinga-2.md +++ b/doc/16-upgrading-icinga-2.md @@ -10,6 +10,9 @@ follow the instructions for v2.7 too. ## Upgrading to v2.12 +* CLI + * New `pki verify` CLI command for better [TLS certificate troubleshooting](15-troubleshooting.md#troubleshooting-certificate-verification) + ### Behavior changes The behavior of multi parent [dependencies](03-monitoring-basics.md#dependencies) was fixed to e.g. render hosts unreachable when both router uplinks are down. diff --git a/lib/base/tlsutility.cpp b/lib/base/tlsutility.cpp index 6529fea64..91723b65b 100644 --- a/lib/base/tlsutility.cpp +++ b/lib/base/tlsutility.cpp @@ -4,6 +4,7 @@ #include "base/convert.hpp" #include "base/logger.hpp" #include "base/context.hpp" +#include "base/convert.hpp" #include "base/utility.hpp" #include "base/application.hpp" #include "base/exception.hpp" @@ -817,6 +818,14 @@ bool VerifyCertificate(const std::shared_ptr& caCertificate, const std::sh X509_STORE_CTX_free(csc); X509_STORE_free(store); + if (rc == 0) { + int err = X509_STORE_CTX_get_error(csc); + + BOOST_THROW_EXCEPTION(openssl_error() + << boost::errinfo_api_function("X509_verify_cert") + << errinfo_openssl_error(err)); + } + return rc == 1; } @@ -837,6 +846,59 @@ bool IsCa(const std::shared_ptr& cacert) #endif /* OPENSSL_VERSION_NUMBER >= 0x10100000L */ } +int GetCertificateVersion(const std::shared_ptr& cert) +{ + return X509_get_version(cert.get()) + 1; +} + +String GetSignatureAlgorithm(const std::shared_ptr& cert) +{ + int alg; + int sign_alg; + X509_PUBKEY *key; + X509_ALGOR *algor; + + key = X509_get_X509_PUBKEY(cert.get()); + + X509_PUBKEY_get0_param(nullptr, nullptr, 0, &algor, key); //TODO: Error handling + + alg = OBJ_obj2nid (algor->algorithm); + +#if OPENSSL_VERSION_NUMBER < 0x10100000L + sign_alg = OBJ_obj2nid((cert.get())->sig_alg->algorithm); +#else /* OPENSSL_VERSION_NUMBER < 0x10100000L */ + sign_alg = X509_get_signature_nid(cert.get()); +#endif /* OPENSSL_VERSION_NUMBER < 0x10100000L */ + + return Convert::ToString((sign_alg == NID_undef) ? "Unknown" : OBJ_nid2ln(sign_alg)); +} + +Array::Ptr GetSubjectAltNames(const std::shared_ptr& cert) +{ + GENERAL_NAMES* subjectAltNames = (GENERAL_NAMES*)X509_get_ext_d2i(cert.get(), NID_subject_alt_name, nullptr, nullptr); + + Array::Ptr sans = new Array(); + + for (int i = 0; i < sk_GENERAL_NAME_num(subjectAltNames); i++) { + GENERAL_NAME* gen = sk_GENERAL_NAME_value(subjectAltNames, i); + if (gen->type == GEN_URI || gen->type == GEN_DNS || gen->type == GEN_EMAIL) { + ASN1_IA5STRING *asn1_str = gen->d.uniformResourceIdentifier; + +#if OPENSSL_VERSION_NUMBER < 0x10100000L + String san = Convert::ToString(ASN1_STRING_data(asn1_str)); +#else /* OPENSSL_VERSION_NUMBER < 0x10100000L */ + String san = Convert::ToString(ASN1_STRING_get0_data(asn1_str)); +#endif /* OPENSSL_VERSION_NUMBER < 0x10100000L */ + + sans->Add(san); + } + } + + GENERAL_NAMES_free(subjectAltNames); + + return sans; +} + std::string to_string(const errinfo_openssl_error& e) { std::ostringstream tmp; diff --git a/lib/base/tlsutility.hpp b/lib/base/tlsutility.hpp index 598e34046..a923311d0 100644 --- a/lib/base/tlsutility.hpp +++ b/lib/base/tlsutility.hpp @@ -6,12 +6,15 @@ #include "base/i2-base.hpp" #include "base/object.hpp" #include "base/shared.hpp" +#include "base/array.hpp" #include "base/string.hpp" #include #include #include #include #include +#include +#include #include #include #include @@ -50,6 +53,9 @@ String RandomString(int length); bool VerifyCertificate(const std::shared_ptr& caCertificate, const std::shared_ptr& certificate); bool IsCa(const std::shared_ptr& cacert); +int GetCertificateVersion(const std::shared_ptr& cert); +String GetSignatureAlgorithm(const std::shared_ptr& cert); +Array::Ptr GetSubjectAltNames(const std::shared_ptr& cert); class openssl_error : virtual public std::exception, virtual public boost::exception { }; diff --git a/lib/cli/CMakeLists.txt b/lib/cli/CMakeLists.txt index 38756b5ce..bbdf801ab 100644 --- a/lib/cli/CMakeLists.txt +++ b/lib/cli/CMakeLists.txt @@ -29,6 +29,7 @@ set(cli_SOURCES pkisavecertcommand.cpp pkisavecertcommand.hpp pkisigncsrcommand.cpp pkisigncsrcommand.hpp pkiticketcommand.cpp pkiticketcommand.hpp + pkiverifycommand.cpp pkiverifycommand.hpp variablegetcommand.cpp variablegetcommand.hpp variablelistcommand.cpp variablelistcommand.hpp variableutility.cpp variableutility.hpp @@ -40,7 +41,7 @@ endif() add_library(cli OBJECT ${cli_SOURCES}) -add_dependencies(cli base config remote) +add_dependencies(cli base config icinga remote) set_target_properties ( cli PROPERTIES diff --git a/lib/cli/pkiverifycommand.cpp b/lib/cli/pkiverifycommand.cpp new file mode 100644 index 000000000..d22d49d91 --- /dev/null +++ b/lib/cli/pkiverifycommand.cpp @@ -0,0 +1,217 @@ +/* Icinga 2 | (c) 2020 Icinga GmbH | GPLv2+ */ + +#include "cli/pkiverifycommand.hpp" +#include "icinga/service.hpp" +#include "remote/pkiutility.hpp" +#include "base/tlsutility.hpp" +#include "base/logger.hpp" +#include + +using namespace icinga; +namespace po = boost::program_options; + +REGISTER_CLICOMMAND("pki/verify", PKIVerifyCommand); + +String PKIVerifyCommand::GetDescription() const +{ + return "Verify TLS certificates: CN, signed by CA, is CA; Print certificate"; +} + +String PKIVerifyCommand::GetShortDescription() const +{ + return "verify TLS certificates: CN, signed by CA, is CA; Print certificate"; +} + +void PKIVerifyCommand::InitParameters(boost::program_options::options_description& visibleDesc, + boost::program_options::options_description& hiddenDesc) const +{ + visibleDesc.add_options() + ("cn", po::value(), "Common Name (optional). Use with '--cert' to check the CN in the certificate.") + ("cert", po::value(), "Certificate file path (optional). Standalone: print certificate. With '--cacert': Verify against CA.") + ("cacert", po::value(), "CA certificate file path (optional). If passed standalone, verifies whether this is a CA certificate"); +} + +std::vector PKIVerifyCommand::GetArgumentSuggestions(const String& argument, const String& word) const +{ + if (argument == "cert" || argument == "cacert") + return GetBashCompletionSuggestions("file", word); + else + return CLICommand::GetArgumentSuggestions(argument, word); +} + +/** + * The entry point for the "pki verify" CLI command. + * + * @returns An exit status. + */ +int PKIVerifyCommand::Run(const boost::program_options::variables_map& vm, const std::vector& ap) const +{ + String cn, certFile, caCertFile; + + if (vm.count("cn")) + cn = vm["cn"].as(); + + if (vm.count("cert")) + certFile = vm["cert"].as(); + + if (vm.count("cacert")) + caCertFile = vm["cacert"].as(); + + /* Verify CN in certificate. */ + if (!cn.IsEmpty() && !certFile.IsEmpty()) { + std::shared_ptr cert; + try { + cert = GetX509Certificate(certFile); + } catch (const std::exception& ex) { + Log(LogCritical, "cli") + << "Cannot read certificate file '" << certFile << "'. Please ensure that it exists and is readable."; + + return ServiceCritical; + } + + Log(LogInformation, "cli") + << "Verifying common name (CN) '" << cn << " in certificate '" << certFile << "'."; + + std::cout << PkiUtility::GetCertificateInformation(cert) << "\n"; + + String certCN = GetCertificateCN(cert); + + if (cn == certCN) { + Log(LogInformation, "cli") + << "OK: CN '" << cn << "' matches certificate CN '" << certCN << "'."; + + return ServiceOK; + } else { + Log(LogCritical, "cli") + << "CRITICAL: CN '" << cn << "' does NOT match certificate CN '" << certCN << "'."; + + return ServiceCritical; + } + } + + /* Verify certificate. */ + if (!certFile.IsEmpty() && !caCertFile.IsEmpty()) { + std::shared_ptr cert; + try { + cert = GetX509Certificate(certFile); + } catch (const std::exception& ex) { + Log(LogCritical, "cli") + << "Cannot read certificate file '" << certFile << "'. Please ensure that it exists and is readable."; + + return ServiceCritical; + } + + std::shared_ptr cacert; + try { + cacert = GetX509Certificate(caCertFile); + } catch (const std::exception& ex) { + Log(LogCritical, "cli") + << "Cannot read CA certificate file '" << caCertFile << "'. Please ensure that it exists and is readable."; + + return ServiceCritical; + } + + Log(LogInformation, "cli") + << "Verifying certificate '" << certFile << "'"; + + std::cout << PkiUtility::GetCertificateInformation(cert) << "\n"; + + Log(LogInformation, "cli") + << " with CA certificate '" << caCertFile << "'."; + + std::cout << PkiUtility::GetCertificateInformation(cacert) << "\n"; + + String certCN = GetCertificateCN(cert); + + bool signedByCA; + + try { + signedByCA = VerifyCertificate(cacert, cert); + } catch (const std::exception& ex) { + Log(LogCritical, "cli") + << "CRITICAL: Certificate with CN '" << certCN << "' is NOT signed by CA: " << DiagnosticInformation(ex, false); + + return ServiceCritical; + } + + if (signedByCA) { + Log(LogInformation, "cli") + << "OK: Certificate with CN '" << certCN << "' is signed by CA."; + + return ServiceOK; + } else { + Log(LogCritical, "cli") + << "CRITICAL: Certificate with CN '" << certCN << "' is NOT signed by CA."; + + return ServiceCritical; + } + } + + + /* Standalone CA checks. */ + if (certFile.IsEmpty() && !caCertFile.IsEmpty()) { + std::shared_ptr cacert; + try { + cacert = GetX509Certificate(caCertFile); + } catch (const std::exception& ex) { + Log(LogCritical, "cli") + << "Cannot read CA certificate file '" << caCertFile << "'. Please ensure that it exists and is readable."; + + return ServiceCritical; + } + + Log(LogInformation, "cli") + << "Checking whether certificate '" << caCertFile << "' is a valid CA certificate."; + + std::cout << PkiUtility::GetCertificateInformation(cacert) << "\n"; + + if (IsCa(cacert)) { + Log(LogInformation, "cli") + << "OK: CA certificate file '" << caCertFile << "' was verified successfully.\n"; + + return ServiceOK; + } else { + Log(LogCritical, "cli") + << "CRITICAL: The file '" << caCertFile << "' does not seem to be a CA certificate file.\n"; + + return ServiceCritical; + } + } + + /* Print certificate */ + if (!certFile.IsEmpty()) { + std::shared_ptr cert; + try { + cert = GetX509Certificate(certFile); + } catch (const std::exception& ex) { + Log(LogCritical, "cli") + << "Cannot read certificate file '" << certFile << "'. Please ensure that it exists and is readable."; + + return ServiceCritical; + } + + Log(LogInformation, "cli") + << "Printing certificate '" << certFile << "'"; + + std::cout << PkiUtility::GetCertificateInformation(cert) << "\n"; + + return ServiceOK; + } + + /* Error handling. */ + if (!cn.IsEmpty() && certFile.IsEmpty()) { + Log(LogCritical, "cli") + << "The '--cn' parameter requires the '--cert' parameter."; + + return ServiceCritical; + } + + if (cn.IsEmpty() && certFile.IsEmpty() && caCertFile.IsEmpty()) { + Log(LogInformation, "cli") + << "Please add the '--help' parameter to see all available options."; + + return ServiceOK; + } + + return ServiceOK; +} diff --git a/lib/cli/pkiverifycommand.hpp b/lib/cli/pkiverifycommand.hpp new file mode 100644 index 000000000..8e4b9dbcb --- /dev/null +++ b/lib/cli/pkiverifycommand.hpp @@ -0,0 +1,32 @@ +/* Icinga 2 | (c) 2020 Icinga GmbH | GPLv2+ */ + +#ifndef PKIVERIFYCOMMAND_H +#define PKIVERIFYCOMMAND_H + +#include "cli/clicommand.hpp" + +namespace icinga +{ + +/** + * The "pki verify" command. + * + * @ingroup cli + */ +class PKIVerifyCommand final : public CLICommand +{ +public: + DECLARE_PTR_TYPEDEFS(PKIVerifyCommand); + + String GetDescription() const override; + String GetShortDescription() const override; + void InitParameters(boost::program_options::options_description& visibleDesc, + boost::program_options::options_description& hiddenDesc) const override; + std::vector GetArgumentSuggestions(const String& argument, const String& word) const override; + int Run(const boost::program_options::variables_map& vm, const std::vector& ap) const override; + +}; + +} + +#endif /* PKIVERIFYCOMMAND_H */ diff --git a/lib/remote/jsonrpcconnection-pki.cpp b/lib/remote/jsonrpcconnection-pki.cpp index dd5154e36..4f9938091 100644 --- a/lib/remote/jsonrpcconnection-pki.cpp +++ b/lib/remote/jsonrpcconnection-pki.cpp @@ -53,7 +53,11 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona String cn = GetCertificateCN(cert); - bool signedByCA = VerifyCertificate(cacert, cert); + bool signedByCA; + + try { + signedByCA = VerifyCertificate(cacert, cert); + } catch (const std::exception&) { } /* Swallow the exception on purpose, cacert will never be a non-CA certificate. */ Log(LogInformation, "JsonRpcConnection") << "Received certificate request for CN '" << cn << "'" @@ -199,7 +203,7 @@ Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictiona * this ensures that the CA we have in /var/lib/icinga2/ca matches the one * we're using for cluster connections (there's no point in sending a client * a certificate it wouldn't be able to use to connect to us anyway) */ - if (!VerifyCertificate(cacert, newcert)) { + if (!signedByCA) { Log(LogWarning, "JsonRpcConnection") << "The CA in '" << listener->GetDefaultCaPath() << "' does not match the CA which Icinga uses " << "for its own cluster connections. This is most likely a configuration problem."; diff --git a/lib/remote/pkiutility.cpp b/lib/remote/pkiutility.cpp index 9b96ca0ea..9adf2d1f9 100644 --- a/lib/remote/pkiutility.cpp +++ b/lib/remote/pkiutility.cpp @@ -13,6 +13,7 @@ #include "base/tcpsocket.hpp" #include "base/json.hpp" #include "base/utility.hpp" +#include "base/convert.hpp" #include "base/exception.hpp" #include "remote/jsonrpc.hpp" #include @@ -321,27 +322,43 @@ String PkiUtility::GetCertificateInformation(const std::shared_ptr& cert) BIO *out = BIO_new(BIO_s_mem()); String pre; - pre = "\n Subject: "; + pre = "\n Version: " + Convert::ToString(GetCertificateVersion(cert)); + BIO_write(out, pre.CStr(), pre.GetLength()); + + pre = "\n Subject: "; BIO_write(out, pre.CStr(), pre.GetLength()); X509_NAME_print_ex(out, X509_get_subject_name(cert.get()), 0, XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB); - pre = "\n Issuer: "; + pre = "\n Issuer: "; BIO_write(out, pre.CStr(), pre.GetLength()); X509_NAME_print_ex(out, X509_get_issuer_name(cert.get()), 0, XN_FLAG_ONELINE & ~ASN1_STRFLGS_ESC_MSB); - pre = "\n Valid From: "; + pre = "\n Valid From: "; BIO_write(out, pre.CStr(), pre.GetLength()); ASN1_TIME_print(out, X509_get_notBefore(cert.get())); - pre = "\n Valid Until: "; + pre = "\n Valid Until: "; BIO_write(out, pre.CStr(), pre.GetLength()); ASN1_TIME_print(out, X509_get_notAfter(cert.get())); - pre = "\n Fingerprint: "; + pre = "\n Serial: "; + BIO_write(out, pre.CStr(), pre.GetLength()); + ASN1_INTEGER *asn1_serial = X509_get_serialNumber(cert.get()); + for (int i = 0; i < asn1_serial->length; i++) { + BIO_printf(out, "%02x%c", asn1_serial->data[i], ((i + 1 == asn1_serial->length) ? '\n' : ':')); + } + + pre = "\n Signature Algorithm: " + GetSignatureAlgorithm(cert); + BIO_write(out, pre.CStr(), pre.GetLength()); + + pre = "\n Subject Alt Names: " + GetSubjectAltNames(cert)->Join(" "); + BIO_write(out, pre.CStr(), pre.GetLength()); + + pre = "\n Fingerprint: "; BIO_write(out, pre.CStr(), pre.GetLength()); unsigned char md[EVP_MAX_MD_SIZE]; unsigned int diglen; - X509_digest(cert.get(), EVP_sha1(), md, &diglen); + X509_digest(cert.get(), EVP_sha256(), md, &diglen); char *data; long length = BIO_get_mem_data(out, &data);