Skip to content

Unexplainable SSL certificate problem: unable to get local issuer certificate (PEER_FAILED_VERIFICATION) on Mac #1303

@ungive

Description

@ungive

Description

Some Mac users of my application (maybe 10%, give or take) experience the following error consistently: SSL certificate problem: unable to get local issuer certificate (PEER_FAILED_VERIFICATION). This issue only happens on Mac. I cannot reproduce it on my own device.

Example/How to Reproduce

  1. Call cpr::MultiPostAsync
  2. See the error

This is my entire code:

#include <iostream>
#include <openssl/opensslv.h>
#include <openssl/crypto.h>
#include <openssl/ssl.h>
#include <openssl/opensslconf.h>

namespace util::request
{
using async_response_type = cpr::AsyncWrapper<cpr::Response, true>;
using shared_async_response_type = std::shared_ptr<async_response_type>;

shared_async_response_type post(std::string const& url,
    cpr::Payload const& payload,
    ungive::scrobbling::NetworkOptions const& options);
}

util::request::shared_async_response_type util::request::post(
    std::string const& url, cpr::Payload const& payload,
    ungive::scrobbling::NetworkOptions const& options)
{
    // Connect timeout: Default is 300000
    // https://curl.se/libcurl/c/CURLOPT_CONNECTTIMEOUT_MS.html
    cpr::ConnectTimeout connect_timeout(300000);
    if (options.connect_timeout.has_value()) {
        connect_timeout = cpr::ConnectTimeout(*options.connect_timeout);
    }

    // Timeout: Default is 0 (no timeout)
    cpr::Timeout timeout = cpr::Timeout(0);
    if (options.timeout.has_value()) {
        timeout = cpr::Timeout(*options.timeout);
    }

    cpr::Verbose verbose{true};

    std::cout << "OPENSSL_VERSION_TEXT: " << OPENSSL_VERSION_TEXT << "\n";
    std::cout << "OpenSSL_version(OPENSSL_DIR): " << OpenSSL_version(OPENSSL_DIR) << "\n";
    std::cout << "OPENSSL_info(OPENSSL_INFO_CONFIG_DIR): " << OPENSSL_info(OPENSSL_INFO_CONFIG_DIR) << "\n";
    std::cout << "X509_get_default_cert_file(): " << X509_get_default_cert_file() << "\n";
    std::cout << "X509_get_default_cert_dir():  " << X509_get_default_cert_dir() << "\n";

    // This will help fix it later.
    // cpr::SslOptions sslOpts = cpr::Ssl(cpr::ssl::CaInfo{"/etc/ssl/cert.pem"});

    auto responses = cpr::MultiPostAsync(
        std::tuple{ cpr::Url{ url }, payload, connect_timeout, timeout, verbose });

    return std::make_shared<cpr::AsyncWrapper<cpr::Response, true>>(
        std::move(responses[0]));
}

Possible Fix

Uncommenting cpr::SslOptions sslOpts = cpr::Ssl(cpr::ssl::CaInfo{"/etc/ssl/cert.pem"}); above and adding sslOpts to the tuple fixes it, which does not make a lot of sense to me because the output of the std::cout statements already print that path (see below).

It should be using /etc/ssl/cert.pem by default. And it does on my system and most other people's system.

Where did you get it from?

conan

Additional Context/Your Environment

I have built CPR via Conan and with OpenSSL. From my conanfile.py:

    requires = (
        "openssl/[~3.0]",
        "cpr/[~1.14]",
        # ...
    )

    def configure(self):
        # ...
        self.options["openssl"].shared = True
        if self.settings.os == "Macos":
            self.options["openssl"].openssldir = "/etc/ssl"

When building with Conan, this selects cpr/1.14.2, libcurl/8.19.0 and openssl/3.0.19.

Output of the log statements in my code:

OPENSSL_VERSION_TEXT: OpenSSL 3.0.19 27 Jan 2026
OpenSSL_version(OPENSSL_DIR): OPENSSLDIR: "/etc/ssl"
OPENSSL_info(OPENSSL_INFO_CONFIG_DIR): /etc/ssl
X509_get_default_cert_file(): /etc/ssl/cert.pem
X509_get_default_cert_dir():  /etc/ssl/certs

When leaving cpr::SslOptions sslOpts = cpr::Ssl(cpr::ssl::CaInfo{"/etc/ssl/cert.pem"}); commented, the verbose logs of the users that have this issue looks like this:

Host ws.audioscrobbler.com:443 was resolved.
IPv6: (none)
IPv4: 130.211.19.189
Trying 130.211.19.189:443...
ALPN: curl offers http/1.1
SSL certificate problem: unable to get local issuer certificate
closing connection #0

When commented out the (i.e. declaring the path to the cert file explicitly even though the default is the same file path), it works (the IP is different because they enabled their VPN again but it didn't work before either with the VPN enabled):

    Host ws.audioscrobbler.com:443 was resolved.
    IPv6: (none)
    IPv4: ......10
    Trying ......10:443...
    ALPN: curl offers http/1.1
    SSL Trust Anchors:
    CAfile: /etc/ssl/cert.pem
    SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / RSASSA-PSS
    ALPN: server accepted http/1.1
    Server certificate:
    subject: C=US; ST=New York; O=CBS Corporation; CN=*.audioscrobbler.com
    start date: Aug 12 00:00:00 2025 GMT
    expire date: Sep 10 23:59:59 2026 GMT
    issuer: C=GB; O=Sectigo Limited; CN=Sectigo Public Server Authentication CA OV R36
    Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
    Certificate level 1: Public key type RSA (3072/128 Bits/secBits), signed using sha384WithRSAEncryption
    Certificate level 2: Public key type RSA (4096/152 Bits/secBits), signed using sha384WithRSAEncryption
    Certificate level 3: Public key type RSA (4096/152 Bits/secBits), signed using sha384WithRSAEncryption
    subjectAltName: "ws.audioscrobbler.com" matches cert's "*.audioscrobbler.com"

    OpenSSL verify result: 0
    SSL certificate verified via OpenSSL.
    Established connection to ws.audioscrobbler.com (198.18.0.10 port 443) from 240.0.0.2 port 57388
    using HTTP/1.x                                                                                   > POST /2.0/ HTTP/1.1

Host: ws.audioscrobbler.com
User-Agent: curl/8.19.0
Accept: /
Accept-Encoding: deflate, gzip
Content-Length: 113
Content-Type: application/x-www-form-urlencoded

    upload completely sent off: 113 bytes

< HTTP/1.1 200 OK
< Server: openresty                                                                                < Date: Thu, 02 Apr 2026 15:40:12 GMT
< Content-Type: text/xml; charset=UTF-8
< Content-Length: 114
< Access-Control-Allow-Methods: POST, GET, OPTIONS
< Access-Control-Allow-Origin: *
< Access-Control-Max-Age: 86400
< Via: 1.1 google
< Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
<

    Connection #0 to host ws.audioscrobbler.com:443 left intact

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions