From a7bf4f791b85bbdedc791b10437c41e14b500ec8 Mon Sep 17 00:00:00 2001 From: Aidan Keefe Date: Tue, 6 Oct 2026 11:11:26 -0600 Subject: [PATCH] Tls Pinning example completed --- .github/examples-manifest.yml | 14 +- .gitignore | 1 + tls/README.md | 112 ++++++++++ tls/client-tls13-pinning.c | 392 ++++++++++++++++++++++++++++++++++ 4 files changed, 517 insertions(+), 2 deletions(-) create mode 100644 tls/client-tls13-pinning.c diff --git a/.github/examples-manifest.yml b/.github/examples-manifest.yml index d36a76a5c..5aacae6d6 100644 --- a/.github/examples-manifest.yml +++ b/.github/examples-manifest.yml @@ -128,10 +128,13 @@ profiles: # absent: it needs the separate wolfAsyncCrypt repo patched in. # keygen + des3: client-tls-pkcs12 needs both (PKCS12 default PBE is 3DES) or # it prints "not configured with ..." and exits 0. + # opensslextra: client-tls13-pinning's README documents it as the way to get + # WOLFSSL_ALWAYS_VERIFY_CB (opensslall alone already implies it via settings.h). flags: >- --enable-tls13 --enable-ech --enable-writedup --enable-pkcallbacks - --enable-postauth --enable-cryptocb --enable-opensslall --enable-session-ticket - --enable-earlydata --enable-keygen --enable-des3 --enable-static --enable-shared + --enable-postauth --enable-cryptocb --enable-opensslall --enable-opensslextra + --enable-session-ticket --enable-earlydata --enable-keygen --enable-des3 + --enable-static --enable-shared cflags: "-DHAVE_SECRET_CALLBACK" dtls: @@ -888,6 +891,13 @@ examples: server_exit: clean expect: "Server: I hear ya fa shizzle" + - pair: + server: [./server-tls13] + client: [./client-tls13-pinning, 127.0.0.1] + stdin: "shutdown\n" + server_exit: clean + expect: "Pin check: server public key matches pin" + - pair: server: [./server-tls-pkcallback] client: [./client-tls-pkcallback, 127.0.0.1] diff --git a/.gitignore b/.gitignore index 145f1b5f0..2d5dd619a 100644 --- a/.gitignore +++ b/.gitignore @@ -88,6 +88,7 @@ android/wolfssljni-ndk-sample/proguard-project.txt /tls/client-tls13 /tls/client-tls13-resume /tls/client-tls13-earlydata +/tls/client-tls13-pinning /tls/server-tls13-earlydata /tls/client-tls-bio /tls/client-tls-cacb diff --git a/tls/README.md b/tls/README.md index 5b0513d6d..45c1ebd54 100644 --- a/tls/README.md +++ b/tls/README.md @@ -119,6 +119,7 @@ into. 3. [Running](#run-ecc) 6. [Encrypted Client Hello](#ech) +7. [Certificate Pinning](#pinning) @@ -1592,6 +1593,117 @@ Expected behavior: processing and again after the handshake is complete. +## Certificate Pinning + +`client-tls13-pinning` is a TLS v1.3 client that pins the server's public key. + +Normal certificate validation accepts any server certificate that chains to a +CA in the trust store. Pinning adds a second check on top of that: the client +ships with the SHA-256 hash of the SubjectPublicKeyInfo (SPKI) of the server it +expects, and refuses the handshake if the server presents any other key, even +one signed by a trusted CA. This defends against a compromised or mis-issuing +CA and against extra roots installed on the device. + +The client pins the public key rather than the whole certificate, so the pin +survives certificate renewal as long as the server keeps the same key. + +How it works: + +1. `wolfSSL_CTX_set_verify()` registers a verify callback. wolfSSL calls it for + the server's leaf certificate after running its own chain validation. If + wolfSSL is built with `WOLFSSL_VERIFY_CB_ALL_CERTS` it also calls it for + each intermediate; the example passes those through unchanged. +2. `wolfSSL_CTX_SetCertCbCtx()` hands the pin to the callback, which receives + it as `store->userCtx`. +3. Inside the callback, `store->certs[0]` holds the DER of the server's + certificate. `wc_GetSubjectPubKeyInfoDerFromCert()` extracts the SPKI and + `wc_Sha256Hash()` hashes it. +4. The example's callback returns 1 only when chain validation passed *and* + the hash matches the pin. Returning 0 aborts the handshake before any + application data is sent. The callback never overrides a chain validation + error, so a pinned key on an expired or untrusted certificate is still + rejected. + +### Building + +The example requires wolfSSL 5.8.2 or later, for +`wc_GetSubjectPubKeyInfoDerFromCert()`, built with TLS v1.3 and SHA-256 +enabled (the defaults). + +wolfSSL must also be built with `WOLFSSL_ALWAYS_VERIFY_CB`. Without it, +wolfSSL only calls the verify callback when chain validation fails, so the pin +would never be checked on a valid chain. `--enable-opensslextra` defines it; +otherwise add it explicitly: + +```sh +./configure CFLAGS="-DWOLFSSL_ALWAYS_VERIFY_CB" && make && sudo make install +``` + +If any of these are missing, the example prints a message saying what it +needs and exits with status 0. + +Then, in `wolfssl-examples/tls`: + +```sh +make server-tls13 client-tls13-pinning +``` + +### Running + +``` +./client-tls13-pinning [pin-sha256-hex] +``` + +The pin defaults to the SPKI hash of `../certs/server-cert.pem`, which is the +certificate `server-tls13` serves. To compute the pin for a different +certificate: + +```sh +openssl x509 -in ../certs/server-cert.pem -pubkey -noout | \ + openssl pkey -pubin -outform DER | openssl dgst -sha256 +``` + +Run the server in one terminal and the client in another: + +```sh +./server-tls13 +``` + +```sh +./client-tls13-pinning 127.0.0.1 +Pin check: server public key matches pin +Connected with TLSv1.3, server public key SHA-256: 5b3efef086a7b4637b2843d0a8fb5c5e7bf1e40b49e182184077f4b1405eddcd +Message for server: hello +Server: I hear ya fa shizzle! + +``` + +Send `shutdown` as the message to stop the server. + +To see the pin reject a server that otherwise validates, pass a pin that does +not match: + +```sh +./client-tls13-pinning 127.0.0.1 0000000000000000000000000000000000000000000000000000000000000000 +Pin check: server public key does not match pin +ERROR: failed to connect to wolfSSL: -329 (verify problem on certificate) + expected pin: 0000000000000000000000000000000000000000000000000000000000000000 + server sent: 5b3efef086a7b4637b2843d0a8fb5c5e7bf1e40b49e182184077f4b1405eddcd +``` + +The handshake is aborted before any application data is exchanged. +`server-tls13` exits after a failed handshake, so restart it before the next +connection. + +Notes: + +- Pinning is brittle. Rotating the server key breaks every client that pinned + the old value, so real deployments pin a backup key as well, or pin an + intermediate CA's key instead of the leaf's. +- `server-tls` defaults to TLS v1.2. Use `server-tls13`, or build `server-tls` + with `-DUSE_TLSV13`, since this client only offers TLS v1.3. + + ## Support Please contact wolfSSL at support@wolfssl.com with any questions, bug fixes, diff --git a/tls/client-tls13-pinning.c b/tls/client-tls13-pinning.c new file mode 100644 index 000000000..14bb8fabd --- /dev/null +++ b/tls/client-tls13-pinning.c @@ -0,0 +1,392 @@ +/* client-tls13-pinning.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfSSL. (formerly known as CyaSSL) + * + * wolfSSL is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * wolfSSL is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/* TLS v1.3 client that pins the server's public key. + * + * Normal certificate validation accepts any server certificate that chains to + * a CA in the trust store. Certificate pinning adds a second check: the + * application ships with the SHA-256 hash of the SubjectPublicKeyInfo (SPKI) + * of the server it expects, and refuses the handshake if the server presents + * any other key, even one signed by a trusted CA. + * + * The pin check runs inside the wolfSSL verify callback, so a mismatch aborts + * the handshake before any application data is sent. + * + * wolfSSL must be built with WOLFSSL_ALWAYS_VERIFY_CB so that the verify + * callback is invoked when chain validation succeeds, not only on failure. + * --enable-opensslextra sets it; otherwise add CFLAGS="-DWOLFSSL_ALWAYS_VERIFY_CB". + */ + +#include +#include +#include + +/* socket includes */ +#include +#include +#include +#include + +/* wolfSSL */ +#ifndef WOLFSSL_USER_SETTINGS + #include +#endif +#include +#include +#include +#include +#include + +#define DEFAULT_PORT 11111 + +#define CERT_FILE "../certs/client-cert.pem" +#define KEY_FILE "../certs/client-key.pem" +#define CA_FILE "../certs/ca-cert.pem" + +/* SHA-256 of the DER-encoded SubjectPublicKeyInfo in ../certs/server-cert.pem. + * In your application this pin would be pre-computed from the cert or certs + * you are wanting to pin + */ +#define DEFAULT_PIN_HEX \ + "5b3efef086a7b4637b2843d0a8fb5c5e7bf1e40b49e182184077f4b1405eddcd" + +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_ALWAYS_VERIFY_CB) && \ + !defined(NO_SHA256) && LIBWOLFSSL_VERSION_HEX >= 0x05008002 + +/* Passed to the verify callback through wolfSSL_CTX_SetCertCbCtx(). */ +typedef struct PinCtx { + unsigned char pinSPKIhash[WC_SHA256_DIGEST_SIZE]; /* expected SPKI hash */ + unsigned char recvSPKIhash[WC_SHA256_DIGEST_SIZE];/* sent by server to us */ + int nonMatchError; /* did error occur */ + int matched; /* pinSPKIhash == recvSPKIhash */ +} PinCtx; + +static void PrintHex(FILE* fp, const unsigned char* buf, size_t sz) +{ + size_t i; + for (i = 0; i < sz; i++) { + fprintf(fp, "%02x", buf[i]); + } +} + +/* Decode exactly outSz bytes of hex. Returns 0 on success, -1 on bad input. */ +static int HexToBytes(const char* hex, unsigned char* out, size_t outSz) +{ + size_t i; + + if (strlen(hex) != outSz * 2) { + return -1; + } + for (i = 0; i < outSz; i++) { + unsigned int v; + if (sscanf(hex + (i * 2), "%2x", &v) != 1) { + return -1; + } + out[i] = (unsigned char)v; + } + return 0; +} + +/* Verify callback. wolfSSL calls it for the server's leaf certificate after + * running its own chain validation (and, with WOLFSSL_VERIFY_CB_ALL_CERTS, for + * each intermediate as well). + * + * preverify: 1 if wolfSSL accepted the certificate, 0 if it found an error + * (store->error holds the code). + * Returns 1 to accept the certificate, 0 to reject it and abort the handshake. + */ +static int PinVerifyCallback(int preverify, WOLFSSL_X509_STORE_CTX* store) +{ + PinCtx* pinCtx = (PinCtx*)store->userCtx; + int ret = 0; + + /* Never override an error wolfSSL found. A pinned key on a certificate + * that failed chain validation (expired, untrusted CA, ...) is still a + * failure. */ + if (!preverify) { + fprintf(stderr, "Certificate verification failed at depth %d: %s\n", + store->error_depth, + wolfSSL_ERR_reason_error_string( + (unsigned long)store->error)); + return 0; + } + + /* Only the server's own certificate (depth 0) carries the pinned key. + * Leave wolfSSL's result for intermediate CA certificates as it is. */ + if (store->error_depth != 0) { + return preverify; + } + + if (pinCtx == NULL || store->certs == NULL || store->totalCerts < 1) { + fprintf(stderr, "Pin check: no peer certificate available\n"); + ret = BAD_FUNC_ARG; + } + + if (ret == 0) { + unsigned char* spki = NULL; + word32 spkiSz = 0; + WOLFSSL_BUFFER_INFO certificateData = store->certs[0]; + + /* First call sizes the SPKI. */ + ret = wc_GetSubjectPubKeyInfoDerFromCert(certificateData.buffer, + certificateData.length, NULL, &spkiSz); + /* ^^^^ NULL here means don't read anything + * just size it into spkiSz */ + if (ret == 0) { + spki = (unsigned char*)malloc(spkiSz); + if (spki == NULL) { + ret = MEMORY_E; + } + } + if (ret == 0) { + /* read SPKI in to spki */ + ret = wc_GetSubjectPubKeyInfoDerFromCert(certificateData.buffer, + certificateData.length, spki, &spkiSz); + } + if (ret == 0) { + /* hash the key */ + ret = wc_Sha256Hash(spki, spkiSz, pinCtx->recvSPKIhash); + } + + free(spki); + } + + if (ret != 0) { + fprintf(stderr, "Pin check: failed to hash server public key: %s\n", + wc_GetErrorString(ret)); + return 0; + } + /* made it here so any error after this is a Match Error */ + pinCtx->nonMatchError = 0; + pinCtx->matched = + (memcmp(pinCtx->recvSPKIhash, pinCtx->pinSPKIhash, + WC_SHA256_DIGEST_SIZE) == 0); + + if (!pinCtx->matched) { + fprintf(stderr, "Pin check: server public key does not match pin\n"); + return 0; + } + + printf("Pin check: server public key matches pin\n"); + return 1; +} + +int main(int argc, char** argv) +{ + int ret = 0; + int sockfd = SOCKET_INVALID; + struct sockaddr_in servAddr; + char buff[256]; + size_t len; + const char* pinHex = DEFAULT_PIN_HEX; + PinCtx pinCtx; + + /* declare wolfSSL objects */ + WOLFSSL_CTX* ctx = NULL; + WOLFSSL* ssl = NULL; + + /* Check for proper calling convention */ + if (argc < 2 || argc > 3) { + printf("usage: %s [pin-sha256-hex]\n", argv[0]); + printf(" pin-sha256-hex: 64 hex digits, the SHA-256 of the server's\n" + " DER SubjectPublicKeyInfo. Defaults to the pin for\n" + " ../certs/server-cert.pem.\n"); + return 0; + } + if (argc == 3) { + pinHex = argv[2]; + } + + memset(&pinCtx, 0, sizeof(pinCtx)); + /* Stays set unless the callback gets as far as comparing the pin */ + pinCtx.nonMatchError = 1; + if (HexToBytes(pinHex, pinCtx.pinSPKIhash, + sizeof(pinCtx.pinSPKIhash)) != 0) { + fprintf(stderr, "ERROR: pin must be %d hex digits\n", + WC_SHA256_DIGEST_SIZE * 2); + return -1; + } + + /* ---- Make Connection To Server (internals arbitrary) ---- */ + if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { + fprintf(stderr, "ERROR: failed to create the socket\n"); + ret = -1; goto exit; + } + + /* Initialize the server address struct with zeros */ + memset(&servAddr, 0, sizeof(servAddr)); + + /* Fill in the server address */ + servAddr.sin_family = AF_INET; /* using IPv4 */ + servAddr.sin_port = htons(DEFAULT_PORT); /* on DEFAULT_PORT */ + + if (inet_pton(AF_INET, argv[1], &servAddr.sin_addr) != 1) { + fprintf(stderr, "ERROR: invalid address\n"); + ret = -1; goto exit; + } + + if ((ret = connect(sockfd, (struct sockaddr*) &servAddr, sizeof(servAddr))) + == -1) { + fprintf(stderr, "ERROR: failed to connect\n"); + goto exit; + } + /* ---- Make Connection To Server (internals arbitrary) ---- */ + + /*---------------------------------*/ + /* Start of wolfSSL initialization and configuration */ + /*---------------------------------*/ + + /* uncomment to enable debug info + * wolfSSL_Debugging_ON(); + */ + + /* Initialize wolfSSL */ + if ((ret = wolfSSL_Init()) != WOLFSSL_SUCCESS) { + fprintf(stderr, "ERROR: Failed to initialize the library\n"); + goto exit; + } + + /* Create and initialize WOLFSSL_CTX, TLS v1.3 only */ + if ((ctx = wolfSSL_CTX_new(wolfTLSv1_3_client_method())) == NULL) { + fprintf(stderr, "ERROR: failed to create WOLFSSL_CTX\n"); + ret = -1; goto exit; + } + + /* Load client certificate into WOLFSSL_CTX (server-tls13 requires it) */ + if ((ret = wolfSSL_CTX_use_certificate_file(ctx, CERT_FILE, + WOLFSSL_FILETYPE_PEM)) != WOLFSSL_SUCCESS) { + fprintf(stderr, "ERROR: failed to load %s, please check the file.\n", + CERT_FILE); + goto exit; + } + + /* Load client key into WOLFSSL_CTX */ + if ((ret = wolfSSL_CTX_use_PrivateKey_file(ctx, KEY_FILE, + WOLFSSL_FILETYPE_PEM)) != WOLFSSL_SUCCESS) { + fprintf(stderr, "ERROR: failed to load %s, please check the file.\n", + KEY_FILE); + goto exit; + } + + /* Load CA certificate into WOLFSSL_CTX. Chain validation still happens; + * the pin is checked on top of it, not instead of it. */ + if ((ret = wolfSSL_CTX_load_verify_locations(ctx, CA_FILE, NULL)) + != WOLFSSL_SUCCESS) { + fprintf(stderr, "ERROR: failed to load %s, please check the file.\n", + CA_FILE); + goto exit; + } + + /* Register the pin check as the verify callback and hand it the pin. + * wolfSSL_CTX_SetCertCbCtx() makes pinCtx available to the callback as + * store->userCtx. */ + wolfSSL_CTX_set_verify(ctx, WOLFSSL_VERIFY_PEER, PinVerifyCallback); + wolfSSL_CTX_SetCertCbCtx(ctx, &pinCtx); + + /* Create a WOLFSSL object */ + if ((ssl = wolfSSL_new(ctx)) == NULL) { + fprintf(stderr, "ERROR: failed to create WOLFSSL object\n"); + ret = -1; goto exit; + } + + /* Attach wolfSSL to the socket */ + if ((ret = wolfSSL_set_fd(ssl, sockfd)) != WOLFSSL_SUCCESS) { + fprintf(stderr, "ERROR: Failed to set the file descriptor\n"); + goto exit; + } + + /* Connect to wolfSSL on the server side */ + if ((ret = wolfSSL_connect(ssl)) != WOLFSSL_SUCCESS) { + int err = wolfSSL_get_error(ssl, ret); + fprintf(stderr, "ERROR: failed to connect to wolfSSL: %d (%s)\n", + err, wolfSSL_ERR_reason_error_string((unsigned long)err)); + if (!pinCtx.nonMatchError && !pinCtx.matched) { + fprintf(stderr, " expected pin: "); + PrintHex(stderr, pinCtx.pinSPKIhash, sizeof(pinCtx.pinSPKIhash)); + fprintf(stderr, "\n server sent: "); + PrintHex(stderr, pinCtx.recvSPKIhash, sizeof(pinCtx.recvSPKIhash)); + fprintf(stderr, "\n"); + } + goto exit; + } + + printf("Connected with %s, server public key SHA-256: ", + wolfSSL_get_version(ssl)); + PrintHex(stdout, pinCtx.recvSPKIhash, sizeof(pinCtx.recvSPKIhash)); + printf("\n"); + + /* Get a message for the server from stdin */ + printf("Message for server: "); + memset(buff, 0, sizeof(buff)); + if (fgets(buff, sizeof(buff), stdin) == NULL) { + fprintf(stderr, "ERROR: failed to get message for server\n"); + ret = -1; goto exit; + } + len = strnlen(buff, sizeof(buff)); + + /* Send the message to the server */ + if ((ret = wolfSSL_write(ssl, buff, (int)len)) != (int)len) { + fprintf(stderr, "ERROR: failed to write entire message\n"); + fprintf(stderr, "%d bytes of %d bytes were sent", ret, (int) len); + goto exit; + } + + /* Read the server data into our buff array */ + memset(buff, 0, sizeof(buff)); + if ((ret = wolfSSL_read(ssl, buff, sizeof(buff)-1)) < 0) { + fprintf(stderr, "ERROR: failed to read\n"); + goto exit; + } + + /* Print to stdout any data the server sends */ + printf("Server: %s\n", buff); + + /* Return reporting a success */ + ret = 0; + +exit: + /* Cleanup and return */ + if (sockfd != SOCKET_INVALID) + close(sockfd); /* Close the connection to the server */ + if (ssl) + wolfSSL_free(ssl); /* Free the wolfSSL object */ + if (ctx) + wolfSSL_CTX_free(ctx); /* Free the wolfSSL context object */ + wolfSSL_Cleanup(); /* Cleanup the wolfSSL environment */ + + return ret; +} + +#else + +int main(int argc, char** argv) +{ + (void)argc; + (void)argv; + printf("Example requires wolfSSL 5.8.2 or later with TLS v1.3, SHA-256 and\n" + "WOLFSSL_ALWAYS_VERIFY_CB\n" + "(build wolfSSL with --enable-opensslextra or\n" + " CFLAGS=\"-DWOLFSSL_ALWAYS_VERIFY_CB\")\n"); + return 0; +} + +#endif /* WOLFSSL_TLS13 && WOLFSSL_ALWAYS_VERIFY_CB && !NO_SHA256 && + * LIBWOLFSSL_VERSION_HEX >= 0x05008002 */