diff --git a/src/args.c b/src/args.c
index f80a046e..4696b537 100644
--- a/src/args.c
+++ b/src/args.c
@@ -309,6 +309,7 @@ void parse_args(int argc, char* argv[])
exit(read_teleporter_zip_from_disk(argv[2]) ? EXIT_SUCCESS : EXIT_FAILURE);
}
+ // Generate X.509 certificate
if(argc > 1 && strcmp(argv[1], "--gen-x509") == 0)
{
if(argc < 3 || argc > 5)
@@ -327,6 +328,55 @@ void parse_args(int argc, char* argv[])
exit(generate_certificate(argv[2], rsa, domain) ? EXIT_SUCCESS : EXIT_FAILURE);
}
+ // Parse X.509 certificate
+ if(argc > 1 &&
+ (strcmp(argv[1], "--read-x509") == 0 ||
+ strcmp(argv[1], "--read-x509-key") == 0))
+ {
+ if(argc < 2 || argc > 4)
+ {
+ printf("Usage: %s %s [] []\n", argv[0], argv[1]);
+ printf("Example: %s %s /etc/pihole/tls.pem\n", argv[0], argv[1]);
+ printf(" with domain: %s %s /etc/pihole/tls.pem pi.hole\n", argv[0], argv[1]);
+ exit(EXIT_FAILURE);
+ }
+
+ // Option parsing
+ // Should we report on the private key?
+ const bool private_key = strcmp(argv[1], "--read-x509-key") == 0;
+ // If no certificate file is given, we use the one from the config
+ const char *certfile = NULL;
+ if(argc == 2)
+ {
+ readFTLconf(&config, false);
+ certfile = config.webserver.tls.cert.v.s;
+ }
+ else
+ certfile = argv[2];
+
+ // If no domain is given, we only check the certificate
+ const char *domain = argc > 3 ? argv[3] : NULL;
+
+ // Enable stdout printing
+ cli_mode = true;
+ log_ctrl(false, true);
+
+ enum cert_check result = read_certificate(certfile, domain, private_key);
+
+ if(argc < 4)
+ exit(result == CERT_OKAY ? EXIT_SUCCESS : EXIT_FAILURE);
+ else if(result == CERT_DOMAIN_MATCH)
+ {
+ printf("Certificate matches domain %s\n", argv[3]);
+ exit(EXIT_SUCCESS);
+ }
+ else
+ {
+ printf("Certificate does not match domain %s\n", argv[3]);
+ exit(EXIT_FAILURE);
+ }
+ }
+
// If the first argument is "gravity" (e.g., /usr/bin/pihole-FTL gravity),
// we offer some specialized gravity tools
if(argc > 1 && (strcmp(argv[1], "gravity") == 0 || strcmp(argv[1], "antigravity") == 0))
@@ -812,6 +862,16 @@ void parse_args(int argc, char* argv[])
printf(" an RSA (4096 bit) key will be generated instead.\n\n");
printf(" Usage: %spihole-FTL --gen-x509 %soutfile %s[rsa]%s\n\n", green, cyan, purple, normal);
+ printf("%sTLS X.509 certificate parser:%s\n", yellow, normal);
+ printf(" Parse the given X.509 certificate and optionally check if\n");
+ printf(" it matches a given domain. If no domain is given, only a\n");
+ printf(" human-readable output string is printed.\n\n");
+ printf(" If no certificate file is given, the one from the config\n");
+ printf(" is used (if applicable). If --read-x509-key is used, details\n");
+ printf(" about the private key are printed as well.\n\n");
+ printf(" Usage: %spihole-FTL --read-x509 %s[certfile] %s[domain]%s\n", green, cyan, purple, normal);
+ printf(" Usage: %spihole-FTL --read-x509-key %s[certfile] %s[domain]%s\n\n", green, cyan, purple, normal);
+
printf("%sGravity tools:%s\n", yellow, normal);
printf(" Check domains in a given file for validity using Pi-hole's\n");
printf(" gravity filters. The expected input format is one domain\n");
diff --git a/src/database/message-table.c b/src/database/message-table.c
index 72ac7b2f..ba1b7591 100644
--- a/src/database/message-table.c
+++ b/src/database/message-table.c
@@ -54,6 +54,8 @@ static const char *get_message_type_str(const enum message_type type)
return "LIST";
case DISK_MESSAGE_EXTENDED:
return "DISK_EXTENDED";
+ case CERTIFICATE_DOMAIN_MISMATCH_MESSAGE:
+ return "CERTIFICATE_DOMAIN_MISMATCH";
case MAX_MESSAGE:
default:
return "UNKNOWN";
@@ -84,6 +86,8 @@ static enum message_type get_message_type_from_string(const char *typestr)
return INACCESSIBLE_ADLIST_MESSAGE;
else if (strcmp(typestr, "DISK_EXTENDED") == 0)
return DISK_MESSAGE_EXTENDED;
+ else if (strcmp(typestr, "CERTIFICATE_DOMAIN_MISMATCH") == 0)
+ return CERTIFICATE_DOMAIN_MISMATCH_MESSAGE;
else
return MAX_MESSAGE;
}
@@ -167,6 +171,14 @@ static unsigned char message_blob_types[MAX_MESSAGE][5] =
SQLITE_TEXT, // File system type
SQLITE_TEXT, // Directory mounted on
SQLITE_NULL // not used
+ },
+ {
+ // CERTIFICATE_DOMAIN_MISMATCH_MESSAGE: The message column contains the certificate file
+ SQLITE_TEXT, // domain
+ SQLITE_NULL, // not used
+ SQLITE_NULL, // not used
+ SQLITE_NULL, // not used
+ SQLITE_NULL // not used
}
};
// Create message table in the database
@@ -333,6 +345,8 @@ static int add_message(const enum message_type type,
case SQLITE_NULL: /* Fall through */
default:
+ log_warn("add_message(type=%s, message=%s) - Excess property, binding NULL",
+ get_message_type_str(type), message);
rc = sqlite3_bind_null(stmt, 3 + j);
break;
}
@@ -653,6 +667,28 @@ static void format_inaccessible_adlist_message(char *plain, const int sizeof_pla
free(escaped_address);
}
+static void format_certificate_domain_mismatch(char *plain, const int sizeof_plain, char *html, const int sizeof_html,
+ const char *certfile, const char*domain)
+{
+ if(snprintf(plain, sizeof_plain, "SSL/TLS certificate %s does not match domain %s!", certfile, domain) > sizeof_plain)
+ log_warn("format_certificate_domain_mismatch(): Buffer too small to hold plain message, warning truncated");
+
+ // Return early if HTML text is not required
+ if(sizeof_html < 1 || html == NULL)
+ return;
+
+ char *escaped_certfile = escape_html(certfile);
+ char *escaped_domain = escape_html(domain);
+
+ if(snprintf(html, sizeof_html, "SSL/TLS certificate %s does not match domain %s!", escaped_certfile, escaped_domain) > sizeof_html)
+ log_warn("format_certificate_domain_mismatch(): Buffer too small to hold HTML message, warning truncated");
+
+ if(escaped_certfile != NULL)
+ free(escaped_certfile);
+ if(escaped_domain != NULL)
+ free(escaped_domain);
+}
+
int count_messages(const bool filter_dnsmasq_warnings)
{
int count = 0;
@@ -876,6 +912,17 @@ bool format_messages(cJSON *array)
break;
}
+
+ case CERTIFICATE_DOMAIN_MISMATCH_MESSAGE:
+ {
+ const char *certfile = (const char*)sqlite3_column_text(stmt, 3);
+ const char *domain = (const char*)sqlite3_column_text(stmt, 4);
+
+ format_certificate_domain_mismatch(plain, sizeof(plain), html, sizeof(html),
+ certfile, domain);
+
+ break;
+ }
}
// Add the plain message
@@ -1095,3 +1142,19 @@ void logg_inaccessible_adlist(const int dbindex, const char *address)
if(rowid == -1)
log_err("logg_inaccessible_adlist(): Failed to add message to database");
}
+
+void log_certificate_domain_mismatch(const char *certfile, const char *domain)
+{
+ // Create message
+ char buf[2048];
+ format_certificate_domain_mismatch(buf, sizeof(buf), NULL, 0, certfile, domain);
+
+ // Log to FTL.log
+ log_warn("%s", buf);
+
+ // Log to database
+ const int rowid = add_message(CERTIFICATE_DOMAIN_MISMATCH_MESSAGE, certfile, 1, domain);
+
+ if(rowid == -1)
+ log_err("log_certificate_domain_mismatch(): Failed to add message to database");
+}
diff --git a/src/database/message-table.h b/src/database/message-table.h
index 52aff56c..196b406a 100644
--- a/src/database/message-table.h
+++ b/src/database/message-table.h
@@ -28,5 +28,6 @@ void logg_rate_limit_message(const char *clientIP, const unsigned int rate_limit
void logg_warn_dnsmasq_message(char *message);
void log_resource_shortage(const double load, const int nprocs, const int shmem, const int disk, const char *path, const char *msg);
void logg_inaccessible_adlist(const int dbindex, const char *address);
+void log_certificate_domain_mismatch(const char *certfile, const char *domain);
#endif //MESSAGETABLE_H
diff --git a/src/enums.h b/src/enums.h
index bfbcff33..65e48182 100644
--- a/src/enums.h
+++ b/src/enums.h
@@ -270,6 +270,7 @@ enum message_type {
DISK_MESSAGE,
INACCESSIBLE_ADLIST_MESSAGE,
DISK_MESSAGE_EXTENDED,
+ CERTIFICATE_DOMAIN_MISMATCH_MESSAGE,
MAX_MESSAGE,
} __attribute__ ((packed));
@@ -311,4 +312,13 @@ enum adlist_type {
ADLIST_ALLOW
} __attribute__ ((packed));
+enum cert_check {
+ CERT_FILE_NOT_FOUND,
+ CERT_CANNOT_PARSE_CERT,
+ CERT_CANNOT_PARSE_KEY,
+ CERT_DOMAIN_MISMATCH,
+ CERT_DOMAIN_MATCH,
+ CERT_OKAY
+} __attribute__ ((packed));
+
#endif // ENUMS_H
diff --git a/src/webserver/http-common.c b/src/webserver/http-common.c
index f7521dac..c1fdc275 100644
--- a/src/webserver/http-common.c
+++ b/src/webserver/http-common.c
@@ -524,6 +524,10 @@ void read_and_parse_payload(struct ftl_conn *api)
// See https://www.w3.org/International/questions/qa-escapes#use
char *__attribute__((malloc)) escape_html(const char *string)
{
+ // If the string is NULL, return NULL
+ if(string == NULL)
+ return NULL;
+
// Allocate memory for escaped string
char *escaped = calloc(strlen(string) * 6 + 1, sizeof(char));
if(!escaped)
diff --git a/src/webserver/webserver.c b/src/webserver/webserver.c
index 59f6d93a..95940e49 100644
--- a/src/webserver/webserver.c
+++ b/src/webserver/webserver.c
@@ -8,24 +8,26 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
-#include "../FTL.h"
-#include "webserver.h"
+#include "FTL.h"
+#include "webserver/webserver.h"
// api_handler()
-#include "../api/api.h"
+#include "api/api.h"
// send_http()
#include "http-common.h"
// struct config
-#include "../config/config.h"
+#include "config/config.h"
// log_web()
-#include "../log.h"
+#include "log.h"
// get_nprocs()
#include
// file_readable()
-#include "../files.h"
+#include "files.h"
// generate_certificate()
-#include "x509.h"
+#include "webserver/x509.h"
// allocate_lua(), free_lua(), init_lua(), request_handler()
-#include "lua_web.h"
+#include "webserver/lua_web.h"
+// log_certificate_domain_mismatch()
+#include "database/message-table.h"
// Server context handle
static struct mg_context *ctx = NULL;
@@ -341,6 +343,10 @@ void http_init(void)
if(file_readable(config.webserver.tls.cert.v.s))
{
+ if(read_certificate(config.webserver.tls.cert.v.s, config.webserver.domain.v.s, false) != CERT_DOMAIN_MATCH)
+ {
+ log_certificate_domain_mismatch(config.webserver.tls.cert.v.s, config.webserver.domain.v.s);
+ }
options[++next_option] = "ssl_certificate";
options[++next_option] = config.webserver.tls.cert.v.s;
diff --git a/src/webserver/x509.c b/src/webserver/x509.c
index eff3ed59..ca0b2140 100644
--- a/src/webserver/x509.c
+++ b/src/webserver/x509.c
@@ -282,3 +282,239 @@ bool generate_certificate(const char* certfile, bool rsa, const char *domain)
return true;
}
+
+// This function reads a X.509 certificate from a file and prints a
+// human-readable representation of the certificate to stdout. If a domain is
+// specified, we only check if this domain is present in the certificate.
+// Otherwise, we print verbose human-readable information about the certificate
+// and about the private key (if requested).
+enum cert_check read_certificate(const char* certfile, const char *domain, const bool private_key)
+{
+ if(certfile == NULL && domain == NULL)
+ {
+ log_err("No certificate file specified\n");
+ return CERT_FILE_NOT_FOUND;
+ }
+
+ mbedtls_x509_crt crt;
+ mbedtls_pk_context key;
+ mbedtls_entropy_context entropy;
+ mbedtls_ctr_drbg_context ctr_drbg;
+ mbedtls_x509_crt_init(&crt);
+ mbedtls_pk_init(&key);
+ mbedtls_entropy_init(&entropy);
+ mbedtls_ctr_drbg_init(&ctr_drbg);
+
+ printf("Reading certificate from %s ...\n\n", certfile);
+
+ // Check if the file exists and is readable
+ if(access(certfile, R_OK) != 0)
+ {
+ log_err("Could not read certificate file: %s\n", strerror(errno));
+ return CERT_FILE_NOT_FOUND;
+ }
+
+ int rc = mbedtls_pk_parse_keyfile(&key, certfile, NULL, mbedtls_ctr_drbg_random, &ctr_drbg);
+ if (rc != 0)
+ {
+ log_err("Cannot parse key: Error code %d\n", rc);
+ return CERT_CANNOT_PARSE_KEY;
+ }
+
+ rc = mbedtls_x509_crt_parse_file(&crt, certfile);
+ if (rc != 0)
+ {
+ log_err("Cannot parse certificate: Error code %d\n", rc);
+ return CERT_CANNOT_PARSE_CERT;
+ }
+
+ // Parse mbedtls_x509_parse_subject_alt_names()
+ mbedtls_x509_sequence *sans = &crt.subject_alt_names;
+ bool found = false;
+ if(domain != NULL)
+ {
+ // Loop over all SANs
+ while(sans != NULL)
+ {
+ // Parse the SAN
+ mbedtls_x509_subject_alternative_name san = { 0 };
+ const int ret = mbedtls_x509_parse_subject_alt_name(&sans->buf, &san);
+
+ // Check if SAN is used (otherwise ret < 0, e.g.,
+ // MBEDTLS_ERR_X509_FEATURE_UNAVAILABLE) and if it is a
+ // DNS name, skip otherwise
+ if(ret < 0 || san.type != MBEDTLS_X509_SAN_DNS_NAME)
+ goto next_san;
+
+ // Check if the SAN matches the domain
+ if(strncasecmp(domain, (char*)san.san.unstructured_name.p, san.san.unstructured_name.len) == 0)
+ {
+ found = true;
+ break;
+ }
+next_san:
+ // Go to next SAN
+ sans = sans->next;
+ }
+
+ // Also check against the common name (CN) field
+ char subject[MBEDTLS_X509_MAX_DN_NAME_SIZE];
+ if(mbedtls_x509_dn_gets(subject, sizeof(subject), &crt.subject) > 0)
+ {
+ // Check subject == "CN="
+ if(strlen(subject) > 3 && strncasecmp(subject, "CN=", 3) == 0 && strcasecmp(domain, subject + 3) == 0)
+ found = true;
+ // Check subject == ""
+ else if(strcasecmp(domain, subject) == 0)
+ found = true;
+ }
+
+
+ // Free resources
+ mbedtls_x509_crt_free(&crt);
+ mbedtls_pk_free(&key);
+ mbedtls_entropy_free(&entropy);
+ mbedtls_ctr_drbg_free(&ctr_drbg);
+ return found ? CERT_DOMAIN_MATCH : CERT_DOMAIN_MISMATCH;
+ }
+
+ // else: Print verbose information about the certificate
+ char certinfo[BUFFER_SIZE] = { 0 };
+ mbedtls_x509_crt_info(certinfo, BUFFER_SIZE, " ", &crt);
+ puts("Certificate (X.509):\n");
+ puts(certinfo);
+
+ if(!private_key)
+ goto end;
+
+ puts("Private key:");
+ const char *keytype = mbedtls_pk_get_name(&key);
+ printf(" Type: %s\n", keytype);
+ mbedtls_pk_type_t pk_type = mbedtls_pk_get_type(&key);
+ if(pk_type == MBEDTLS_PK_RSA)
+ {
+ mbedtls_rsa_context *rsa = mbedtls_pk_rsa(key);
+ printf(" RSA modulus: %zu bit\n", 8*mbedtls_rsa_get_len(rsa));
+ mbedtls_mpi E, N, P, Q, D;
+ mbedtls_mpi_init(&E); // E = public exponent (public)
+ mbedtls_mpi_init(&N); // N = P * Q (public)
+ mbedtls_mpi_init(&P); // P = prime factor 1 (private)
+ mbedtls_mpi_init(&Q); // Q = prime factor 2 (private)
+ mbedtls_mpi_init(&D); // D = private exponent (private)
+ mbedtls_mpi DP, DQ, QP;
+ mbedtls_mpi_init(&DP);
+ mbedtls_mpi_init(&DQ);
+ mbedtls_mpi_init(&QP);
+ if(mbedtls_rsa_export(rsa, &N, &P, &Q, &D, &E) != 0 ||
+ mbedtls_rsa_export_crt(rsa, &DP, &DQ, &QP) != 0)
+ {
+ puts(" could not export RSA parameters\n");
+ return EXIT_FAILURE;
+ }
+ puts(" Core parameters:");
+ if(mbedtls_mpi_write_file(" Exponent:\n E = 0x", &E, 16, NULL) != 0)
+ {
+ puts(" could not write MPI\n");
+ return EXIT_FAILURE;
+ }
+
+ if(mbedtls_mpi_write_file(" Modulus:\n N = 0x", &N, 16, NULL) != 0)
+ {
+ puts(" could not write MPI\n");
+ return EXIT_FAILURE;
+ }
+
+ if(mbedtls_mpi_cmp_mpi(&P, &Q) >= 0)
+ {
+ if(mbedtls_mpi_write_file(" Prime factors:\n P = 0x", &P, 16, NULL) != 0 ||
+ mbedtls_mpi_write_file(" Q = 0x", &Q, 16, NULL) != 0)
+ {
+ puts(" could not write MPIs\n");
+ return EXIT_FAILURE;
+ }
+ }
+ else
+ {
+ if(mbedtls_mpi_write_file(" Prime factors:\n Q = 0x", &Q, 16, NULL) != 0 ||
+ mbedtls_mpi_write_file("\n P = 0x", &P, 16, NULL) != 0)
+ {
+ puts(" could not write MPIs\n");
+ return EXIT_FAILURE;
+ }
+ }
+
+ if(mbedtls_mpi_write_file(" Private exponent:\n D = 0x", &D, 16, NULL) != 0)
+ {
+ puts(" could not write MPI\n");
+ return EXIT_FAILURE;
+ }
+
+ mbedtls_mpi_free(&N);
+ mbedtls_mpi_free(&P);
+ mbedtls_mpi_free(&Q);
+ mbedtls_mpi_free(&D);
+ mbedtls_mpi_free(&E);
+
+ puts(" CRT parameters:");
+ if(mbedtls_mpi_write_file(" D mod (P-1):\n DP = 0x", &DP, 16, NULL) != 0 ||
+ mbedtls_mpi_write_file(" D mod (Q-1):\n DQ = 0x", &DQ, 16, NULL) != 0 ||
+ mbedtls_mpi_write_file(" Q^-1 mod P:\n QP = 0x", &QP, 16, NULL) != 0)
+ {
+ puts(" could not write MPIs\n");
+ return EXIT_FAILURE;
+ }
+
+ mbedtls_mpi_free(&DP);
+ mbedtls_mpi_free(&DQ);
+ mbedtls_mpi_free(&QP);
+
+ }
+ else if(pk_type == MBEDTLS_PK_ECKEY)
+ {
+ mbedtls_ecp_keypair *ec = mbedtls_pk_ec(key);
+ mbedtls_ecp_curve_type ec_type = mbedtls_ecp_get_type(&ec->private_grp);
+ switch (ec_type)
+ {
+ case MBEDTLS_ECP_TYPE_NONE:
+ puts(" Curve type: Unknown");
+ break;
+ case MBEDTLS_ECP_TYPE_SHORT_WEIERSTRASS:
+ puts(" Curve type: Short Weierstrass (y^2 = x^3 + a x + b)");
+ break;
+ case MBEDTLS_ECP_TYPE_MONTGOMERY:
+ puts(" Curve type: Montgomery (y^2 = x^3 + a x^2 + x)");
+ break;
+ }
+ const size_t bitlen = mbedtls_mpi_bitlen(&ec->private_d);
+ printf(" Bitlen: %zu bit\n", bitlen);
+
+ mbedtls_mpi_write_file(" Private key:\n D = 0x", &ec->private_d, 16, NULL);
+ mbedtls_mpi_write_file(" Public key:\n X = 0x", &ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(X), 16, NULL);
+ mbedtls_mpi_write_file(" Y = 0x", &ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Y), 16, NULL);
+ mbedtls_mpi_write_file(" Z = 0x", &ec->MBEDTLS_PRIVATE(Q).MBEDTLS_PRIVATE(Z), 16, NULL);
+ }
+ else
+ {
+ puts("Sorry, but FTL does not know how to print key information for this type\n");
+ goto end;
+ }
+
+ // Print private key in PEM format
+ mbedtls_pk_write_key_pem(&key, (unsigned char*)certinfo, BUFFER_SIZE);
+ puts("Private key (PEM):");
+ puts(certinfo);
+
+end:
+ // Print public key in PEM format
+ mbedtls_pk_write_pubkey_pem(&key, (unsigned char*)certinfo, BUFFER_SIZE);
+ puts("Public key (PEM):");
+ puts(certinfo);
+
+ // Free resources
+ mbedtls_x509_crt_free(&crt);
+ mbedtls_pk_free(&key);
+ mbedtls_entropy_free(&entropy);
+ mbedtls_ctr_drbg_free(&ctr_drbg);
+
+ return CERT_OKAY;
+}
diff --git a/src/webserver/x509.h b/src/webserver/x509.h
index ad97537a..e59ee1a7 100644
--- a/src/webserver/x509.h
+++ b/src/webserver/x509.h
@@ -13,6 +13,9 @@
#include
#include
+#include "enums.h"
+
bool generate_certificate(const char* certfile, bool rsa, const char *domain);
+enum cert_check read_certificate(const char* certfile, const char *domain, const bool private_key);
#endif // X509_H
diff --git a/test/test_suite.bats b/test/test_suite.bats
index b5de84d8..fd264cf4 100644
--- a/test/test_suite.bats
+++ b/test/test_suite.bats
@@ -1368,6 +1368,89 @@
run bash -c 'curl -I --cacert /etc/pihole/test.crt --resolve pi.hole:443:127.0.0.1 https://pi.hole/'
}
+@test "X.509 certificate parser returns expected result" {
+ # We are getting the certificate from the config
+ run bash -c './pihole-FTL --read-x509'
+ printf "%s\n" "${lines[@]}"
+ [[ "${lines[0]}" == "Reading certificate from /etc/pihole/test.pem ..." ]]
+ [[ "${lines[1]}" == "Certificate (X.509):" ]]
+ [[ "${lines[2]}" == " cert. version : 3" ]]
+ [[ "${lines[3]}" == " serial number : 30:36:35:35:38:30:34:30:38:32:39:39:39:31:36" ]]
+ [[ "${lines[4]}" == " issuer name : CN=pi.hole" ]]
+ [[ "${lines[5]}" == " subject name : CN=pi.hole" ]]
+ [[ "${lines[6]}" == " issued on : 2001-01-01 00:00:00" ]]
+ [[ "${lines[7]}" == " expires on : 2030-12-31 23:59:59" ]]
+ [[ "${lines[8]}" == " signed using : ECDSA with SHA256" ]]
+ [[ "${lines[9]}" == " EC key size : 521 bits" ]]
+ [[ "${lines[10]}" == " basic constraints : CA=false" ]]
+ [[ "${lines[11]}" == "Public key (PEM):" ]]
+ [[ "${lines[12]}" == "-----BEGIN PUBLIC KEY-----" ]]
+ [[ "${lines[13]}" == "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBQ51HeOLjSap1Xr+pnFQJqvBZc92T" ]]
+ [[ "${lines[14]}" == "XyL4KwIZdpsHl95Pc0Xcn8Xzyox0cWhMyycQgcGbIw3nuefCZaXfc3CuU30BPDdb" ]]
+ [[ "${lines[15]}" == "91h+rDhV4+VkEkANPBbgKQ6kCiHNtMAdugyaeHxzFpqegGGvgQ2l4Vp98l4M7zBC" ]]
+ [[ "${lines[16]}" == "G6K/RbZDlDvNUCgwElE=" ]]
+ [[ "${lines[17]}" == "-----END PUBLIC KEY-----" ]]
+ [[ "${lines[18]}" == "" ]]
+}
+
+@test "X.509 certificate parser returns expected result (with private key)" {
+ # We are explicitly specifying the certificate file here
+ run bash -c './pihole-FTL --read-x509-key /etc/pihole/test.pem'
+ printf "%s\n" "${lines[@]}"
+ [[ "${lines[0]}" == "Reading certificate from /etc/pihole/test.pem ..." ]]
+ [[ "${lines[1]}" == "Certificate (X.509):" ]]
+ [[ "${lines[2]}" == " cert. version : 3" ]]
+ [[ "${lines[3]}" == " serial number : 30:36:35:35:38:30:34:30:38:32:39:39:39:31:36" ]]
+ [[ "${lines[4]}" == " issuer name : CN=pi.hole" ]]
+ [[ "${lines[5]}" == " subject name : CN=pi.hole" ]]
+ [[ "${lines[6]}" == " issued on : 2001-01-01 00:00:00" ]]
+ [[ "${lines[7]}" == " expires on : 2030-12-31 23:59:59" ]]
+ [[ "${lines[8]}" == " signed using : ECDSA with SHA256" ]]
+ [[ "${lines[9]}" == " EC key size : 521 bits" ]]
+ [[ "${lines[10]}" == " basic constraints : CA=false" ]]
+ [[ "${lines[11]}" == "Private key:" ]]
+ [[ "${lines[12]}" == " Type: EC" ]]
+ [[ "${lines[13]}" == " Curve type: Short Weierstrass (y^2 = x^3 + a x + b)" ]]
+ [[ "${lines[14]}" == " Bitlen: 518 bit" ]]
+ [[ "${lines[15]}" == " Private key:" ]]
+ [[ "${lines[16]}" == " D = 0x2CBE6CF8A913B445F211165B0473B7037B5B06187C8685AEF4A58354C7061C388173E0B00374A55CEAC7BB5886159C9D54B3C020564355A0FA71A55559304156D8"* ]]
+ [[ "${lines[17]}" == " Public key:" ]]
+ [[ "${lines[18]}" == " X = 0x01439D4778E2E349AA755EBFA99C5409AAF05973DD935F22F82B0219769B0797DE4F7345DC9FC5F3CA8C7471684CCB271081C19B230DE7B9E7C265A5DF7370AE537D"* ]]
+ [[ "${lines[19]}" == " Y = 0x013C375BF7587EAC3855E3E56412400D3C16E0290EA40A21CDB4C01DBA0C9A787C73169A9E8061AF810DA5E15A7DF25E0CEF30421BA2BF45B643943BCD5028301251"* ]]
+ [[ "${lines[20]}" == " Z = 0x01"* ]]
+ [[ "${lines[21]}" == "Private key (PEM):" ]]
+ [[ "${lines[22]}" == "-----BEGIN EC PRIVATE KEY-----" ]]
+ [[ "${lines[23]}" == "MIHcAgEBBEIALL5s+KkTtEXyERZbBHO3A3tbBhh8hoWu9KWDVMcGHDiBc+CwA3Sl" ]]
+ [[ "${lines[24]}" == "XOrHu1iGFZydVLPAIFZDVaD6caVVWTBBVtigBwYFK4EEACOhgYkDgYYABAFDnUd4" ]]
+ [[ "${lines[25]}" == "4uNJqnVev6mcVAmq8Flz3ZNfIvgrAhl2mweX3k9zRdyfxfPKjHRxaEzLJxCBwZsj" ]]
+ [[ "${lines[26]}" == "Dee558Jlpd9zcK5TfQE8N1v3WH6sOFXj5WQSQA08FuApDqQKIc20wB26DJp4fHMW" ]]
+ [[ "${lines[27]}" == "mp6AYa+BDaXhWn3yXgzvMEIbor9FtkOUO81QKDASUQ==" ]]
+ [[ "${lines[28]}" == "-----END EC PRIVATE KEY-----" ]]
+ [[ "${lines[29]}" == "Public key (PEM):" ]]
+ [[ "${lines[30]}" == "-----BEGIN PUBLIC KEY-----" ]]
+ [[ "${lines[31]}" == "MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBQ51HeOLjSap1Xr+pnFQJqvBZc92T" ]]
+ [[ "${lines[32]}" == "XyL4KwIZdpsHl95Pc0Xcn8Xzyox0cWhMyycQgcGbIw3nuefCZaXfc3CuU30BPDdb" ]]
+ [[ "${lines[33]}" == "91h+rDhV4+VkEkANPBbgKQ6kCiHNtMAdugyaeHxzFpqegGGvgQ2l4Vp98l4M7zBC" ]]
+ [[ "${lines[34]}" == "G6K/RbZDlDvNUCgwElE=" ]]
+ [[ "${lines[35]}" == "-----END PUBLIC KEY-----" ]]
+ [[ "${lines[36]}" == "" ]]
+}
+
+@test "X.509 certificate parser can check if domain is included" {
+ run bash -c './pihole-FTL --read-x509-key /etc/pihole/test.pem pi.hole'
+ printf "%s\n" "${lines[@]}"
+ [[ "${lines[0]}" == "Reading certificate from /etc/pihole/test.pem ..." ]]
+ [[ "${lines[1]}" == "Certificate matches domain pi.hole" ]]
+ [[ "${lines[2]}" == "" ]]
+ [[ $status == 0 ]]
+ run bash -c './pihole-FTL --read-x509-key /etc/pihole/test.pem pi-hole.net'
+ printf "%s\n" "${lines[@]}"
+ [[ "${lines[0]}" == "Reading certificate from /etc/pihole/test.pem ..." ]]
+ [[ "${lines[1]}" == "Certificate does not match domain pi-hole.net" ]]
+ [[ "${lines[2]}" == "" ]]
+ [[ $status == 1 ]]
+}
+
@test "Test embedded GZIP compressor" {
run bash -c './pihole-FTL gzip test/pihole-FTL.db.sql'
printf "Compression output:\n"