MINOR: ssl/cli: 'show ssl cert' give information on the certificates

Implement the 'show ssl cert' command on the CLI which list the frontend
certificates. With a certificate name in parameter it will show more
details.
diff --git a/doc/management.txt b/doc/management.txt
index d1a1103..973b6f3 100644
--- a/doc/management.txt
+++ b/doc/management.txt
@@ -2507,6 +2507,37 @@
   $ echo "show stat json" | socat /var/run/haproxy.sock stdio | \
     python -m json.tool
 
+show ssl cert [<filename>]
+  Display the list of certicates used on frontends. If a filename is prefixed
+  by an asterisk, it is a transaction which is not commited yet.  If a
+  filename is specified, it will show details about the certificate. This
+  command can be useful to check if a certificate was well updated. You can
+  also display details on a transaction by prefixing the filename by an
+  asterisk.
+
+  Example :
+
+    $ echo "@1 show ssl cert" | socat /var/run/haproxy.master -
+    # transaction
+    *test.local.pem
+    # filename
+    test.local.pem
+
+    $ echo "@1 show ssl cert test.local.pem" | socat /var/run/haproxy.master -
+    Filename: test.local.pem
+    Serial: 03ECC19BA54B25E85ABA46EE561B9A10D26F
+    notBefore: Sep 13 21:20:24 2019 GMT
+    notAfter: Dec 12 21:20:24 2019 GMT
+    Issuer: /C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3
+    Subject: /CN=test.local
+    Subject Alternative Name: DNS:test.local, DNS:imap.test.local
+    Algorithm: RSA2048
+    SHA1 FingerPrint: 417A11CAE25F607B24F638B4A8AEE51D1E211477
+
+    $ echo "@1 show ssl cert *test.local.pem" | socat /var/run/haproxy.master -
+    Filename: *test.local.pem
+    [...]
+
 show resolvers [<resolvers section id>]
   Dump statistics for the given resolvers section, or all resolvers sections
   if no section is supplied.
diff --git a/src/ssl_sock.c b/src/ssl_sock.c
index 0a52972..568bf3b 100644
--- a/src/ssl_sock.c
+++ b/src/ssl_sock.c
@@ -6862,23 +6862,14 @@
 	}
 }
 
-/* used for ppv2 pkey alog (can be used for logging) */
-int ssl_sock_get_pkey_algo(struct connection *conn, struct buffer *out)
+/* fill a buffer with the algorithm and size of a public key */
+static int cert_get_pkey_algo(X509 *crt, struct buffer *out)
 {
-	struct ssl_sock_ctx *ctx;
 	int bits = 0;
 	int sig = TLSEXT_signature_anonymous;
 	int len = -1;
-	X509 *crt;
 	EVP_PKEY *pkey;
 
-	if (!ssl_sock_is_ssl(conn))
-		return 0;
-	ctx = conn->xprt_ctx;
-
-	crt = SSL_get_certificate(ctx->ssl);
-	if (!crt)
-		return 0;
 	pkey = X509_get_pubkey(crt);
 	if (pkey) {
 		bits = EVP_PKEY_bits(pkey);
@@ -6914,6 +6905,24 @@
 	return 1;
 }
 
+/* used for ppv2 pkey alog (can be used for logging) */
+int ssl_sock_get_pkey_algo(struct connection *conn, struct buffer *out)
+{
+	struct ssl_sock_ctx *ctx;
+	X509 *crt;
+
+	if (!ssl_sock_is_ssl(conn))
+		return 0;
+
+	ctx = conn->xprt_ctx;
+
+	crt = SSL_get_certificate(ctx->ssl);
+	if (!crt)
+		return 0;
+
+	return cert_get_pkey_algo(crt, out);
+}
+
 /* used for ppv2 cert signature (can be used for logging) */
 const char *ssl_sock_get_cert_sig(struct connection *conn)
 {
@@ -7111,7 +7120,37 @@
 
 	return 0;
 
+}
+
+/*
+ * Extract and format the DNS SAN extensions and copy result into a chuink
+ * Return 0;
+ */
+#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
+static int ssl_sock_get_san_oneline(X509 *cert, struct buffer *out)
+{
+	int i;
+	char *str;
+	STACK_OF(GENERAL_NAME) *names = NULL;
+
+	names = X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL);
+	if (names) {
+		for (i = 0; i < sk_GENERAL_NAME_num(names); i++) {
+			GENERAL_NAME *name = sk_GENERAL_NAME_value(names, i);
+			if (i > 0)
+				chunk_appendf(out, ", ");
+			if (name->type == GEN_DNS) {
+				if (ASN1_STRING_to_UTF8((unsigned char **)&str, name->d.dNSName) >= 0) {
+					chunk_appendf(out, "DNS:%s", str);
+					OPENSSL_free(str);
+				}
+			}
+		}
+		sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free);
+	}
+	return 0;
 }
+#endif
 
 /* Extract and format full DN from a X509_NAME and copy result into a chunk
  * Returns 1 if dn entries exits, 0 if no dn entry found or -1 if output is not large enough.
@@ -10137,6 +10176,225 @@
 	SETCERT_ST_FIN,
 };
 
+/* release function of the  `show ssl cert' command */
+static void cli_release_show_cert(struct appctx *appctx)
+{
+	HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
+}
+
+/* IO handler of "show ssl cert <filename>" */
+static int cli_io_handler_show_cert(struct appctx *appctx)
+{
+	struct buffer *trash = alloc_trash_chunk();
+	struct ebmb_node *node;
+	struct stream_interface *si = appctx->owner;
+	struct ckch_store *ckchs;
+	int n;
+
+	if (trash == NULL)
+		return 1;
+
+	if (!appctx->ctx.ssl.old_ckchs) {
+		if (ckchs_transaction.old_ckchs) {
+			ckchs = ckchs_transaction.old_ckchs;
+			chunk_appendf(trash, "# transaction\n");
+			if (!ckchs->multi) {
+				chunk_appendf(trash, "*%s\n", ckchs->path);
+			} else {
+				chunk_appendf(trash, "*%s:", ckchs->path);
+				for (n = 0; n < SSL_SOCK_NUM_KEYTYPES; n++) {
+					if (ckchs->ckch[n].cert)
+						chunk_appendf(trash, " %s.%s\n", ckchs->path, SSL_SOCK_KEYTYPE_NAMES[n]);
+				}
+				chunk_appendf(trash, "\n");
+			}
+		}
+	}
+
+	if (!appctx->ctx.cli.p0) {
+		chunk_appendf(trash, "# filename\n");
+		node = ebmb_first(&ckchs_tree);
+	} else {
+		node = &((struct ckch_store *)appctx->ctx.cli.p0)->node;
+	}
+	while (node) {
+		ckchs = ebmb_entry(node, struct ckch_store, node);
+		if (!ckchs->multi) {
+			chunk_appendf(trash, "%s\n", ckchs->path);
+		} else {
+			chunk_appendf(trash, "%s:", ckchs->path);
+			for (n = 0; n < SSL_SOCK_NUM_KEYTYPES; n++) {
+				if (ckchs->ckch[n].cert)
+					chunk_appendf(trash, " %s.%s", ckchs->path, SSL_SOCK_KEYTYPE_NAMES[n]);
+			}
+			chunk_appendf(trash, "\n");
+		}
+
+		node = ebmb_next(node);
+		if (ci_putchk(si_ic(si), trash) == -1) {
+			si_rx_room_blk(si);
+			goto yield;
+		}
+	}
+
+	appctx->ctx.cli.p0 = NULL;
+	free_trash_chunk(trash);
+	return 1;
+yield:
+
+	free_trash_chunk(trash);
+	appctx->ctx.cli.p0 = ckchs;
+	return 0; /* should come back */
+}
+
+/* IO handler of the details "show ssl cert <filename>" */
+static int cli_io_handler_show_cert_detail(struct appctx *appctx)
+{
+	struct stream_interface *si = appctx->owner;
+	struct ckch_store *ckchs = appctx->ctx.cli.p0;
+	struct buffer *out = alloc_trash_chunk();
+	struct buffer *tmp = alloc_trash_chunk();
+	X509_NAME *name = NULL;
+	int write = -1;
+	BIO *bio = NULL;
+
+	if (!tmp || !out)
+		goto end;
+
+	if (!ckchs->multi) {
+		chunk_appendf(out, "Filename: ");
+		if (ckchs == ckchs_transaction.new_ckchs)
+			chunk_appendf(out, "*");
+		chunk_appendf(out, "%s\n", ckchs->path);
+		chunk_appendf(out, "Serial: ");
+		if (ssl_sock_get_serial(ckchs->ckch->cert, tmp) == -1)
+			goto end;
+		dump_binary(out, tmp->area, tmp->data);
+		chunk_appendf(out, "\n");
+
+		chunk_appendf(out, "notBefore: ");
+		chunk_reset(tmp);
+		if ((bio = BIO_new(BIO_s_mem())) ==  NULL)
+			goto end;
+		if (ASN1_TIME_print(bio, X509_getm_notBefore(ckchs->ckch->cert)) == 0)
+			goto end;
+		write = BIO_read(bio, tmp->area, tmp->size-1);
+		tmp->area[write] = '\0';
+		BIO_free(bio);
+		chunk_appendf(out, "%s\n", tmp->area);
+
+		chunk_appendf(out, "notAfter: ");
+		chunk_reset(tmp);
+		if ((bio = BIO_new(BIO_s_mem())) == NULL)
+			goto end;
+		if (ASN1_TIME_print(bio, X509_getm_notAfter(ckchs->ckch->cert)) == 0)
+			goto end;
+		if ((write = BIO_read(bio, tmp->area, tmp->size-1)) <= 0)
+			goto end;
+		tmp->area[write] = '\0';
+		BIO_free(bio);
+		chunk_appendf(out, "%s\n", tmp->area);
+
+
+		chunk_appendf(out, "Issuer: ");
+		if ((name = X509_get_issuer_name(ckchs->ckch->cert)) == NULL)
+			goto end;
+		if ((ssl_sock_get_dn_oneline(name, tmp)) == -1)
+			goto end;
+		*(tmp->area + tmp->data) = '\0';
+		chunk_appendf(out, "%s\n", tmp->area);
+
+		chunk_appendf(out, "Subject: ");
+		if ((name = X509_get_subject_name(ckchs->ckch->cert)) == NULL)
+			goto end;
+		if ((ssl_sock_get_dn_oneline(name, tmp)) == -1)
+			goto end;
+		*(tmp->area + tmp->data) = '\0';
+		chunk_appendf(out, "%s\n", tmp->area);
+
+
+#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME
+		chunk_appendf(out, "Subject Alternative Name: ");
+		if (ssl_sock_get_san_oneline(ckchs->ckch->cert, out) == -1)
+		    goto end;
+		*(out->area + out->data) = '\0';
+		chunk_appendf(out, "\n");
+#endif
+		chunk_reset(tmp);
+		chunk_appendf(out, "Algorithm: ");
+		if (cert_get_pkey_algo(ckchs->ckch->cert, tmp) == 0)
+			goto end;
+		chunk_appendf(out, "%s\n", tmp->area);
+
+		chunk_reset(tmp);
+		chunk_appendf(out, "SHA1 FingerPrint: ");
+		if (X509_digest(ckchs->ckch->cert, EVP_sha1(), (unsigned char *) tmp->area,
+		                (unsigned int *)&tmp->data) == 0)
+			goto end;
+		dump_binary(out, tmp->area, tmp->data);
+		chunk_appendf(out, "\n");
+	}
+
+	if (ci_putchk(si_ic(si), out) == -1) {
+		si_rx_room_blk(si);
+		goto yield;
+	}
+
+end:
+	free_trash_chunk(tmp);
+	free_trash_chunk(out);
+	return 1;
+yield:
+	free_trash_chunk(tmp);
+	free_trash_chunk(out);
+	return 0; /* should come back */
+}
+
+/* parsing function for 'show ssl cert [certfile]' */
+static int cli_parse_show_cert(char **args, char *payload, struct appctx *appctx, void *private)
+{
+	struct ckch_store *ckchs;
+
+	if (!cli_has_level(appctx, ACCESS_LVL_OPER))
+		return cli_err(appctx, "Can't allocate memory!\n");
+
+	/* The operations on the CKCH architecture are locked so we can
+	 * manipulate ckch_store and ckch_inst */
+	if (HA_SPIN_TRYLOCK(CKCH_LOCK, &ckch_lock))
+		return cli_err(appctx, "Can't show!\nOperations on certificates are currently locked!\n");
+
+	/* check if there is a certificate to lookup */
+	if (*args[3]) {
+		if (*args[3] == '*') {
+			if (!ckchs_transaction.new_ckchs)
+				goto error;
+
+			ckchs = ckchs_transaction.new_ckchs;
+
+			if (strcmp(args[3] + 1, ckchs->path))
+				goto error;
+
+		} else {
+			if ((ckchs = ckchs_lookup(args[3])) == NULL)
+				goto error;
+
+		}
+
+		if (ckchs->multi)
+			goto error;
+
+		appctx->ctx.cli.p0 = ckchs;
+		/* use the IO handler that shows details */
+		appctx->io_handler = cli_io_handler_show_cert_detail;
+	}
+
+	return 0;
+
+error:
+	HA_SPIN_UNLOCK(CKCH_LOCK, &ckch_lock);
+	return cli_err(appctx, "Can't display the certificate: Not found or the certificate is a bundle!\n");
+}
+
 /* release function of the  `set ssl cert' command, free things and unlock the spinlock */
 static void cli_release_commit_cert(struct appctx *appctx)
 {
@@ -10859,6 +11117,7 @@
 	{ { "set", "ssl", "cert", NULL }, "set ssl cert <certfile> <payload> : replace a certificate file", cli_parse_set_cert, NULL, NULL },
 	{ { "commit", "ssl", "cert", NULL }, "commit ssl cert <certfile> : commit a certificate file", cli_parse_commit_cert, cli_io_handler_commit_cert, cli_release_commit_cert },
 	{ { "abort", "ssl", "cert", NULL }, "abort ssl cert <certfile> : abort a transaction for a certificate file", cli_parse_abort_cert, NULL, NULL },
+	{ { "show", "ssl", "cert", NULL }, "show ssl cert [<certfile>] : display the SSL certificates used in memory, or the details of a <certfile>", cli_parse_show_cert, cli_io_handler_show_cert, cli_release_show_cert },
 	{ { NULL }, NULL, NULL, NULL }
 }};