From: Bob Beck Subject: Re: nc/libtls: peer cert pubkey fingerprint printing To: Theo Buehler Cc: Bob Beck , tech@openbsd.org, beck@openbsd.org, jsing@openbsd.org Date: Wed, 6 Nov 2024 04:56:30 -0700 > On Nov 6, 2024, at 12:27 AM, Theo Buehler wrote: > > 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 > + > +#include > #include > #include > > @@ -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. So ahh - this is a little bit bonkers. How would we ever practically change this, once software starts using this stuff. IMO it would make a little more sense to just put sha256 in the name of this and outright say it is sha256 If we ever decide that we have to support needless sha3 bullshit, or defend against a kardeshev-3 civilization, we can just add a separate API. So, my two cents - tls_peer_cert_pubkey_sha256, etc. or something like that. > +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);