Index | Thread | Search

From:
Theo Buehler <tb@theobuehler.org>
Subject:
nc/libtls: peer cert pubkey fingerprint printing
To:
tech@openbsd.org
Cc:
beck@openbsd.org, jsing@openbsd.org
Date:
Wed, 6 Nov 2024 08:27:04 +0100

Download raw body.

Thread
For things like github you may want to pin its certificate in a
.gitconfig stanza like this one:

[http "https://github.com/"]
        pinnedpubkey = sha256//Gs+dT9kUC17nDYZXH52mKzGnlUU/Q5mS0UruTQW3H0U=

As you can see, this uses a somewhat weird libcurlism with that sha256//
prefixed base64 blob (thus the entire thing is a valid base64 string, but
that's probably a coincidence).

It is somewhat annoying to compute this hash with what used to be the
world's most terrible command line interface before git was inflicted
on us. So here's a diff that adds a getter to libtls to compute this
thing and uses this getter in nc(1) to display it like so

$ nc -cvz github.com 443
[...]
Pubkey Fingerprint: sha256//Gs+dT9kUC17nDYZXH52mKzGnlUU/Q5mS0UruTQW3H0U=

When we discussed it in Melbourne, it was suggested I do a SHA256:$hex
version of this, which will be easy enough to do, but I have no need for
this, and I forgot the details, so I need to be told again, preferably
with suggested naming for the public API.

Of note, this hashes the entire SPKI, not only the bitstring like in
the SKI, so X509_pubkey_digest() is of no use. Also, I tried to be
consistent by mentioning pubkey in the public facing parts to avoid
confusion with the -fingerprint option that computes the hash of the
entire cert.

Of course this needs a minor bump (not included). This isn't urgent
and I'll wait for the next bump, e.g., when bluhm and henning finish
what they're cooking.

Index: lib/libtls/Symbols.list
===================================================================
RCS file: /cvs/src/lib/libtls/Symbols.list,v
diff -u -p -r1.16 Symbols.list
--- lib/libtls/Symbols.list	24 Mar 2022 15:56:34 -0000	1.16
+++ lib/libtls/Symbols.list	6 Nov 2024 06:14:59 -0000
@@ -71,6 +71,7 @@ tls_ocsp_process_response
 tls_peer_cert_chain_pem
 tls_peer_cert_contains_name
 tls_peer_cert_hash
+tls_peer_cert_pubkey_fingerprint
 tls_peer_cert_issuer
 tls_peer_cert_notafter
 tls_peer_cert_notbefore
Index: lib/libtls/tls.c
===================================================================
RCS file: /cvs/src/lib/libtls/tls.c,v
diff -u -p -r1.104 tls.c
--- lib/libtls/tls.c	8 Apr 2024 20:47:32 -0000	1.104
+++ lib/libtls/tls.c	6 Nov 2024 06:19:38 -0000
@@ -336,6 +336,42 @@ tls_cert_pubkey_hash(X509 *cert, char **
 	return (rv);
 }
 
+int
+tls_cert_pubkey_fingerprint(X509 *cert, char **fingerprint)
+{
+	X509_PUBKEY *pubkey;
+	char d[EVP_MAX_MD_SIZE], *dbase64 = NULL;
+	unsigned char *der = NULL;
+	int derlen = 0, dlen = 0, rv = -1;
+
+	free(*fingerprint);
+	*fingerprint = NULL;
+
+	if ((pubkey = X509_get_X509_PUBKEY(cert)) == NULL)
+		goto err;
+	if ((derlen = i2d_X509_PUBKEY(pubkey, &der)) <= 0) {
+		derlen = 0;
+		goto err;
+	}
+	if (!EVP_Digest(der, derlen, d, &dlen, EVP_sha256(), NULL))
+		goto err;
+	if (tls_base64_string(d, dlen, &dbase64, NULL) != 0)
+		goto err;
+
+	if (asprintf(fingerprint, "sha256//%s", dbase64) == -1) {
+		*fingerprint = NULL;
+		goto err;
+	}
+
+	rv = 0;
+
+ err:
+	freezero(der, derlen);
+	free(dbase64);
+
+	return rv;
+}
+
 static int
 tls_keypair_to_pkey(struct tls *ctx, struct tls_keypair *keypair, EVP_PKEY **pkey)
 {
Index: lib/libtls/tls.h
===================================================================
RCS file: /cvs/src/lib/libtls/tls.h,v
diff -u -p -r1.67 tls.h
--- lib/libtls/tls.h	2 Aug 2024 15:00:01 -0000	1.67
+++ lib/libtls/tls.h	6 Nov 2024 06:15:29 -0000
@@ -201,6 +201,7 @@ int tls_peer_cert_provided(struct tls *_
 int tls_peer_cert_contains_name(struct tls *_ctx, const char *_name);
 
 const char *tls_peer_cert_hash(struct tls *_ctx);
+const char *tls_peer_cert_pubkey_fingerprint(struct tls *_ctx);
 const char *tls_peer_cert_issuer(struct tls *_ctx);
 const char *tls_peer_cert_subject(struct tls *_ctx);
 time_t	tls_peer_cert_notbefore(struct tls *_ctx);
Index: lib/libtls/tls_conninfo.c
===================================================================
RCS file: /cvs/src/lib/libtls/tls_conninfo.c,v
diff -u -p -r1.27 tls_conninfo.c
--- lib/libtls/tls_conninfo.c	26 Mar 2024 06:31:22 -0000	1.27
+++ lib/libtls/tls_conninfo.c	6 Nov 2024 06:13:03 -0000
@@ -16,6 +16,9 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <netinet/in.h>
+
+#include <resolv.h>
 #include <stdio.h>
 #include <string.h>
 
@@ -42,6 +45,43 @@ tls_convert_notafter(struct tm *tm, time
 }
 
 int
+tls_base64_string(const unsigned char *in, size_t inlen, char **out,
+    size_t *outlen)
+{
+	char *base64 = NULL;
+	size_t base64_len = 0;
+	int ret = -1;
+
+	if (outlen != NULL)
+		*outlen = 0;
+	if (*out != NULL)
+		goto err;
+
+	if (inlen > INT_MAX / 4)
+		goto err;
+
+	base64_len = ((inlen + 2) / 3) * 4 + 1;
+	if ((base64 = calloc(1, base64_len)) == NULL)
+		goto err;
+
+	if (b64_ntop(in, inlen, base64, base64_len) == -1)
+		goto err;
+
+	*out = base64;
+	base64 = NULL;
+
+	if (outlen != NULL)
+		*outlen = base64_len;
+
+	ret = 0;
+
+ err:
+	freezero(base64, base64_len);
+
+	return (ret);
+}
+
+int
 tls_hex_string(const unsigned char *in, size_t inlen, char **out,
     size_t *outlen)
 {
@@ -87,6 +127,21 @@ tls_get_peer_cert_hash(struct tls *ctx, 
 }
 
 static int
+tls_get_peer_cert_fingerprint(struct tls *ctx, char **fingerprint)
+{
+	*fingerprint = NULL;
+	if (ctx->ssl_peer_cert == NULL)
+		return (0);
+
+	if (tls_cert_pubkey_fingerprint(ctx->ssl_peer_cert, fingerprint) == -1) {
+		tls_set_errorx(ctx, TLS_ERROR_OUT_OF_MEMORY, "out of memory");
+		*fingerprint = NULL;
+		return (-1);
+	}
+	return (0);
+}
+
+static int
 tls_get_peer_cert_issuer(struct tls *ctx,  char **issuer)
 {
 	X509_NAME *name = NULL;
@@ -152,6 +207,8 @@ tls_get_peer_cert_info(struct tls *ctx)
 	if (ctx->ssl_peer_cert == NULL)
 		return (0);
 
+	if (tls_get_peer_cert_fingerprint(ctx, &ctx->conninfo->fingerprint) == -1)
+		goto err;
 	if (tls_get_peer_cert_hash(ctx, &ctx->conninfo->hash) == -1)
 		goto err;
 	if (tls_get_peer_cert_subject(ctx, &ctx->conninfo->subject) == -1)
@@ -298,6 +355,7 @@ tls_conninfo_free(struct tls_conninfo *c
 	free(conninfo->servername);
 	free(conninfo->version);
 
+	free(conninfo->fingerprint);
 	free(conninfo->hash);
 	free(conninfo->issuer);
 	free(conninfo->subject);
Index: lib/libtls/tls_internal.h
===================================================================
RCS file: /cvs/src/lib/libtls/tls_internal.h,v
diff -u -p -r1.85 tls_internal.h
--- lib/libtls/tls_internal.h	26 Mar 2024 06:24:52 -0000	1.85
+++ lib/libtls/tls_internal.h	6 Nov 2024 06:08:18 -0000
@@ -129,6 +129,7 @@ struct tls_conninfo {
 	int session_resumed;
 	char *version;
 
+	char *fingerprint;
 	char *hash;
 	char *issuer;
 	char *subject;
@@ -291,10 +292,13 @@ int tls_ocsp_verify_cb(SSL *ssl, void *a
 int tls_ocsp_stapling_cb(SSL *ssl, void *arg);
 void tls_ocsp_free(struct tls_ocsp *ctx);
 struct tls_ocsp *tls_ocsp_setup_from_peer(struct tls *ctx);
+int tls_base64_string(const unsigned char *_in, size_t _inlen, char **_out,
+    size_t *_outlen);
 int tls_hex_string(const unsigned char *_in, size_t _inlen, char **_out,
     size_t *_outlen);
 int tls_cert_hash(X509 *_cert, char **_hash);
 int tls_cert_pubkey_hash(X509 *_cert, char **_hash);
+int tls_cert_pubkey_fingerprint(X509 *_cert, char **_fingerprint);
 
 int tls_password_cb(char *_buf, int _size, int _rwflag, void *_u);
 
Index: lib/libtls/tls_peer.c
===================================================================
RCS file: /cvs/src/lib/libtls/tls_peer.c,v
diff -u -p -r1.8 tls_peer.c
--- lib/libtls/tls_peer.c	10 Apr 2017 17:11:13 -0000	1.8
+++ lib/libtls/tls_peer.c	6 Nov 2024 06:15:08 -0000
@@ -30,6 +30,15 @@ tls_peer_cert_hash(struct tls *ctx)
 		return (NULL);
 	return (ctx->conninfo->hash);
 }
+
+const char *
+tls_peer_cert_pubkey_fingerprint(struct tls *ctx)
+{
+	if (ctx->conninfo == NULL)
+		return (NULL);
+	return (ctx->conninfo->fingerprint);
+}
+
 const char *
 tls_peer_cert_issuer(struct tls *ctx)
 {
Index: lib/libtls/man/tls_conn_version.3
===================================================================
RCS file: /cvs/src/lib/libtls/man/tls_conn_version.3,v
diff -u -p -r1.10 tls_conn_version.3
--- lib/libtls/man/tls_conn_version.3	2 Nov 2019 13:43:14 -0000	1.10
+++ lib/libtls/man/tls_conn_version.3	6 Nov 2024 06:50:31 -0000
@@ -31,6 +31,7 @@
 .Nm tls_peer_cert_issuer ,
 .Nm tls_peer_cert_subject ,
 .Nm tls_peer_cert_hash ,
+.Nm tls_peer_cert_pubkey_fingerprint ,
 .Nm tls_peer_cert_notbefore ,
 .Nm tls_peer_cert_notafter
 .Nd inspect an established TLS connection
@@ -66,6 +67,8 @@
 .Fn tls_peer_cert_subject "struct tls *ctx"
 .Ft const char *
 .Fn tls_peer_cert_hash "struct tls *ctx"
+.Ft const char *
+.Fn tls_peer_cert_pubkey_fingerprint "struct tls *ctx"
 .Ft time_t
 .Fn tls_peer_cert_notbefore "struct tls *ctx"
 .Ft time_t
@@ -149,6 +152,22 @@ h=$(openssl x509 -outform der -in mycert
 printf "SHA256:${h}\\n"
 .Ed
 .Pp
+.Fn tls_peer_cert_pubkey_fingerprint
+returns a string
+corresponding to a hash of the raw certificate's subject public key info from
+.Ar ctx
+prefixed by a hash name followed by two slashes.
+The hash currently used is SHA256, though this
+could change in the future.
+The public key fingerprint for a certificate in file
+.Ar mycert.crt
+can be generated using the commands:
+.Bd -literal -offset indent
+f=$(openssl x509 -pubkey -in mycert.crt |
+    openssl pkey -pubin -outform der | sha256 -b)
+printf "sha256//${f}\\n"
+.Ed
+.Pp
 .Fn tls_peer_cert_notbefore
 returns the time corresponding to the start of the validity period of
 the peer certificate from
@@ -209,6 +228,9 @@ appeared in
 .Fn tls_conn_cipher_strength
 appeared in
 .Ox 6.7 .
+.Fn tls_peer_cert_pubkey_fingerprint
+appeared in
+.Ox 7.7 .
 .Sh AUTHORS
 .An Bob Beck Aq Mt beck@openbsd.org
 .An Joel Sing Aq Mt jsing@openbsd.org
Index: regress/lib/libtls/gotls/tls.go
===================================================================
RCS file: /cvs/src/regress/lib/libtls/gotls/tls.go,v
diff -u -p -r1.16 tls.go
--- regress/lib/libtls/gotls/tls.go	2 Aug 2024 15:02:22 -0000	1.16
+++ regress/lib/libtls/gotls/tls.go	6 Nov 2024 06:25:21 -0000
@@ -220,6 +220,15 @@ func (t *TLS) PeerCertHash() (string, er
 	return C.GoString(hash), nil
 }
 
+// PeerCertPubkeyFingerprint returns a hash of the peer certificate's public key.
+func (t *TLS) PeerCertPubkeyFingerprint() (string, error) {
+	hash := C.tls_peer_cert_pubkey_fingerprint(t.ctx)
+	if hash == nil {
+		return "", errors.New("no hash returned")
+	}
+	return C.GoString(hash), nil
+}
+
 // PeerCertNotBefore returns the notBefore time from the peer
 // certificate.
 func (t *TLS) PeerCertNotBefore() (time.Time, error) {
Index: regress/lib/libtls/gotls/tls_test.go
===================================================================
RCS file: /cvs/src/regress/lib/libtls/gotls/tls_test.go,v
diff -u -p -r1.10 tls_test.go
--- regress/lib/libtls/gotls/tls_test.go	2 Jul 2023 06:37:27 -0000	1.10
+++ regress/lib/libtls/gotls/tls_test.go	6 Nov 2024 06:28:03 -0000
@@ -18,6 +18,7 @@ const (
 	httpContent = "Hello, TLS!"
 
 	certHash = "SHA256:1153aa0230ee0481b36bdd83ddb04b607340dbda35f3a4fff0615e4d9292d687"
+	certFingerprint = "sha256//ybtCgKrAwBjEc3yEHzrre0RnI/Yre8JIF1cF4vxY4Cg="
 )
 
 var (
@@ -421,6 +422,9 @@ func TestTLSInfo(t *testing.T) {
 	if _, err := tls.PeerCertHash(); err == nil {
 		t.Error("PeerCertHash() returned nil error, want error")
 	}
+	if _, err := tls.PeerCertPubkeyFingerprint(); err == nil {
+		t.Error("PeerCertHash() returned nil error, want error")
+	}
 	if _, err := tls.PeerCertNotBefore(); err == nil {
 		t.Error("PeerCertNotBefore() returned nil error, want error")
 	}
@@ -474,6 +478,13 @@ func TestTLSInfo(t *testing.T) {
 		t.Errorf("Got cert hash %q, want %q", hash, certHash)
 	} else {
 		t.Logf("Hash: %v", hash)
+	}
+	if fingerprint, err := tls.PeerCertPubkeyFingerprint(); err != nil {
+		t.Errorf("PeerCertPubkeyFingerprint() returned error: %v", err)
+	} else if fingerprint != certFingerprint {
+		t.Errorf("Got cert hash %q, want %q", fingerprint, certFingerprint)
+	} else {
+		t.Logf("Pubkey Fingerprint: %v", fingerprint)
 	}
 	if notBefore, err := tls.PeerCertNotBefore(); err != nil {
 		t.Errorf("PeerCertNotBefore() returned error: %v", err)
Index: usr.bin/nc/netcat.c
===================================================================
RCS file: /cvs/src/usr.bin/nc/netcat.c,v
diff -u -p -r1.229 netcat.c
--- usr.bin/nc/netcat.c	2 Nov 2024 17:19:27 -0000	1.229
+++ usr.bin/nc/netcat.c	6 Nov 2024 06:21:42 -0000
@@ -1737,6 +1737,9 @@ report_tls(struct tls *tls_ctx, char *ho
 	if (tls_peer_cert_hash(tls_ctx))
 		fprintf(stderr, "Cert Hash: %s\n",
 		    tls_peer_cert_hash(tls_ctx));
+	if (tls_peer_cert_pubkey_fingerprint(tls_ctx))
+		fprintf(stderr, "Pubkey Fingerprint: %s\n",
+		    tls_peer_cert_pubkey_fingerprint(tls_ctx));
 	ocsp_url = tls_peer_ocsp_url(tls_ctx);
 	if (ocsp_url != NULL)
 		fprintf(stderr, "OCSP URL: %s\n", ocsp_url);