APIs saying either 'unable to get local issuer certificate' or 'self-signed certificate in certificate chain'

I recently updated my certificates using certify the web, and after that I get errors trying to access APIs using PHP and curl.

Server is Windows 11/Apache 2.4.65/PHP 8.4.12.

Error messages are either ‘self-signed certificate in certificate chain’ or ‘unable to get local issuer certificate’, depending on which API I try to use.

Any Ideas?

This means your apache config is not pointing to the latest certificate files and instead they are pointing to the default self signed certificates that exist on your machine.

Certify Certificate Manager does not update your apache config, it just renews certificates and optionally exports those certificates as files you can use for specific apps (like Apache), if you configure it to do so.

You should review your apache config for ssl to see which files they point to.

Thank you for your response!

My httpd.conf file contains these lines:

SSLEngine                on
SSLCertificateFile       ./cert/lparker/certificate.crt
SSLCertificateKeyFile    ./cert/lparker/private.key
SSLCertificateChainFile  ./cert/lparker/ca_bundle.crt

which do point to the cert files that were installed automatically by the certify the web deployment. And, this did work before the latest cert renewal. I have not made any changes to the apache config. I have done renewals for this site before and not had a problem. There is a 4th file that was installed at the same time as the other three called cacert.pem, but I find no reference to it in the apache config file.

You should review your deployment task parameter configuration (for the Deploy to Apache to Deploy to Generic Server task) in Certify to ensure those are the files it is exporting.

I’ve deleted the cert files, then run CTW and confirmed they were recreated in the folder were Apache was looking for them. But I think I’ve been attacking this from the wrong end.

I changed the addresses of the APIs I’m using from https to http, and now two out of the three are working. The one the does not work gives the error message ‘unable to get local issuer certificate’.

It seems to be an issue with remote site certificate, and the fact I noticed it shortly after renewing my own certificate was a coincidence.

If you change to http then there will be no certificate used at all but a lot of things refuse to work over http nowadays and require https by default.

If this is a public API that is accessible of the internet you can use https://chainchecker.certifytheweb.com/ to get a quick summary of the certificate chain being presented by your API. There are other tools that can do the same things.

If working locally and you have access to openssl you can try
openssl s_client -showcerts -connect api.yourdomain.com:443 to see the certificates being served.

When you setup a certificate on a server you are pointing to teh certificate itself (the “leaf/end-entity” certificate) which covers the name of your service. That in turn is signed by an “intermediate” certificate from the CA, and that’s included in the bundle. The intermediate is in turn signed by the CAs root certificate.

Certificate trusts works by having the CAs latest root certificates in your systems local trust store, so for some clients that’s hte OS machine certificate store, and for others it’s a ca-bundle (list of trusted roots) installed by the app or library.

“unable to get local issuer certificate” means either the certificate being served is invalid or self-signed, or your client doesn’t know that root (needs an update).

Thanks again for your responses!

I ran this https://chainchecker.certifytheweb.com/ against my site, and against the three API sites I’m trying to use. My site and two of the API sites said ‘certificate chain ok’. For the other API site it said ‘This Let’s Encrypt chain uses the newer ISRG Root X1 root, which is trusted by current operating systems. This chain may cause issues for some old devices, particularly Android 7.1 and lower.’

I installed openssl and ran it against these same four sites. Three of them returned this message: Verify Error: num=20: unable to get local issuer certificate. The fourth one (which was the odd one above), returned this: ‘Verify Error 19: self-signed certificate in chain’.

Here are the openssl details from when I ran it against my own site:

CONNECTED(00000200)
depth=1 C=US, O=Let’s Encrypt, CN=R13
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN=(MyDomain)
verify return:1

Certificate chain
0 s:CN=(MyDomain)
i:C=US, O=Let’s Encrypt, CN=R13
a:PKEY: RSA, 2048 (bit); sigalg: sha256WithRSAEncryption
v:NotBefore: Jan 29 19:06:14 2026 GMT; NotAfter: Apr 29 19:06:13 2026 GMT
-----BEGIN CERTIFICATE-----
(Details Omitted)
-----END CERTIFICATE-----
1 s:C=US, O=Let’s Encrypt, CN=R13
i:C=US, O=Internet Security Research Group, CN=ISRG Root X1
a:PKEY: RSA, 2048 (bit); sigalg: sha256WithRSAEncryption
v:NotBefore: Mar 13 00:00:00 2024 GMT; NotAfter: Mar 12 23:59:59 2027 GMT
-----BEGIN CERTIFICATE-----
(Details Omitted)
-----END CERTIFICATE-----

Server certificate
subject=CN=(MyDomain)
issuer=C=US, O=Let’s Encrypt, CN=R13

No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: rsa_pss_rsae_sha256
Negotiated TLS1.3 group: X25519MLKEM768

SSL handshake has read 4220 bytes and written 1622 bytes
Verification error: unable to get local issuer certificate

New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Protocol: TLSv1.3
Server public key is 2048 bit
This TLS version forbids renegotiation.
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 20 (unable to get local issuer certificate)


Post-Handshake New Session Ticket arrived:
SSL-Session:
Protocol : TLSv1.3
Cipher : TLS_AES_256_GCM_SHA384
Session-ID: 9A9DB3DBF10775A5DF54E99F5225552B337C1BF0708E60EC482125EE374C2393
Session-ID-ctx:
Resumption PSK: 125E1F3E2D8BD8218A314A8CA1AB5E293AD8810BA17FA322CE36F993B1BF4FFF95D9216F62CF6917570780C858664EF5
PSK identity: None
PSK identity hint: None
SRP username: None
TLS session ticket lifetime hint: 300 (seconds)
TLS session ticket:
(Details Omitted)

Start Time: 1770834461
Timeout   : 7200 (sec)
Verify return code: 20 (unable to get local issuer certificate)
Extended master secret: no
Max Early Data: 0

read R BLOCK

Post-Handshake New Session Ticket arrived:
SSL-Session:
Protocol : TLSv1.3
Cipher : TLS_AES_256_GCM_SHA384
Session-ID: 0F1750DB4A611CE185EAC83DBA9F36C701785CFA3AA12EA14770F3372681D976
Session-ID-ctx:
Resumption PSK: 409FA9BB4BA4654743E052B6A269B168EA5E1E0F93AAA4B041804AB77849D42E0045DC988DCE493C8352A923C2FAAB33
PSK identity: None
PSK identity hint: None
SRP username: None
TLS session ticket lifetime hint: 300 (seconds)
TLS session ticket:

Start Time: 1770834461
Timeout   : 7200 (sec)
Verify return code: 20 (unable to get local issuer certificate)
Extended master secret: no
Max Early Data: 0

read R BLOCK
(Waits 60 Seconds, then types:)
closed

I really do not know where to go from here.

Can you double check that your deployment task to export the certificate is exporting the “full chain” certificate file and that your apache config is pointing to that file for the certificate.

If you are using the full chain certiicate file you shouldn’t need the line that says

SSLCertificateChainFile  ./cert/lparker/ca_bundle.crt

My deployment task is setup as:

Output filepath for cert - d:\Apache24\cert\lparker\cacert.pem
Output filepath for key - d:\Apache24\cert\lparker\private.key
Output filepath for full chain - d:\Apache24\cert\lparker\certificate.crt
Output filepath for CA chain - d:\Apache24\cert\lparker\ca_bundle.crt

The httpd.conf file contains these lines:

SSLEngine on
SSLCertificateFile ./cert/lparker/certificate.crt
SSLCertificateKeyFile ./cert/lparker/private.key
SSLCertificateChainFile ./cert/lparker/ca_bundle.crt

I tried commenting out that last one but it did not make a difference that I could see.

Looks good, comment out the SSLCertificateChainFile line and restart Apache.

If it still fails can you share the contents of the full chain file. There are public certs so the content is already public.

Commenting out the line ‘SSLCertificateChainFile…’ doesn’t make a difference that I can see.

Here are the contents of certificate.crt:

-----BEGIN CERTIFICATE-----
MIIE+jCCA+KgAwIBAgISBnKyDHHkOHXldyd5+ehCRbuIMA0GCSqGSIb3DQEBCwUA
MDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD
EwNSMTMwHhcNMjYwMTI5MTkwNjE0WhcNMjYwNDI5MTkwNjEzWjAWMRQwEgYDVQQD
EwtscGFya2VyLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKli
GJb1eZ6ikoKsRodZ8Fw4bbw2i2dpxEIQR1pSFcky80TZpSlKSFoPlUNCpEja4u3Q
lXE6NNGOmd3hz+glBMVIgtHGVc/qGBIenFipI9FFOqZbJ5QWqg33aYQd+FMZr1bt
30R/FcAO5Y86Rs5f2/f0jSXgEtGbv4xesROWAqV98zsDEmJFgBkjN+4k2SL9NoCw
8Z/EZ8qO9taNKzrVdqnH7XOYAcbuMSbzK55zfgNPyg8hEHy7JND8VQCyDKpZePks
BTTftSbJ6Cj7fj0L/0Oxgp2eSMqPX+2PtEH+Aci/RZ3KbJrKFV1hhxfg2BiC+c//
PpkwkqpkWh9irlKs3vMCAwEAAaOCAiMwggIfMA4GA1UdDwEB/wQEAwIFoDAdBgNV
HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E
FgQUSz7KpP183lNWcr9QXFNmvHj4w6QwHwYDVR0jBBgwFoAU56ufDywzoFPTXk94
yLKEDjvWkjMwMwYIKwYBBQUHAQEEJzAlMCMGCCsGAQUFBzAChhdodHRwOi8vcjEz
LmkubGVuY3Iub3JnLzAWBgNVHREEDzANggtscGFya2VyLm5ldDATBgNVHSAEDDAK
MAgGBmeBDAECATAtBgNVHR8EJjAkMCKgIKAehhxodHRwOi8vcjEzLmMubGVuY3Iu
b3JnLzkuY3JsMIIBDQYKKwYBBAHWeQIEAgSB/gSB+wD5AHYAyzj3FYl8hKFEX1vB
3fvJbvKaWc1HCmkFhbDLFMMUWOcAAAGcC1uzMwAABAMARzBFAiEAtQHszoIUbm6R
DuMC/PsnX2sfe6g0vIsS+dDYjMOtwWQCIGrTuJfF1PA4sso0VpFbK/aYO2ZYUQYI
RPAbtqTtjRZRAH8AcX6V88I4im2x44RJPTHhWqliCHYtQgDgBQzQZ7WmYeIAAAGc
C1uzUAAIAAAFAAgkglIEAwBIMEYCIQCWjqhKtWacnCDgN//WQCCYeE5508t6NcKZ
TuL1e8orCgIhAMria7+uXHC5aOhS5LHgLfbLjUSm0XfS2Wn0cYZOSEuSMA0GCSqG
SIb3DQEBCwUAA4IBAQBJwaThEIG3dfC7lqvz7FGjFPmGku6bd2uxN3o6MjJOHxy3
qrGnccA2Fz9jSKrE0VgDP2Eq429V6jOPY7Wi3w53SlRkvxsbQ+gZVxr3WzqTAiC1
Yl8UXcNxg0ehuLkZSNAFU3tuy1+g0QjQh1x59DWsyYTdvsfukzQIDdbILhQwtMUL
y9V5vJxz9n8x2lO714yC9DKl4bpiWdBsl8p5BBDv3FhwwAm6roDaNarPqeoxJ6hY
y+MyUg+jS4/beG9ypwbAzTQMaiL2vT1B6eh6pkeauJsJOeI/LREKjMDwq5IgrmUa
7ibMtEC96k1I6F7sZcHCblsWyec6CjdcQd8Vzkgw
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFBTCCAu2gAwIBAgIQWgDyEtjUtIDzkkFX6imDBTANBgkqhkiG9w0BAQsFADBP
MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy
Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNDAzMTMwMDAwMDBa
Fw0yNzAzMTIyMzU5NTlaMDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBF
bmNyeXB0MQwwCgYDVQQDEwNSMTMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQClZ3CN0FaBZBUXYc25BtStGZCMJlA3mBZjklTb2cyEBZPs0+wIG6BgUUNI
fSvHSJaetC3ancgnO1ehn6vw1g7UDjDKb5ux0daknTI+WE41b0VYaHEX/D7YXYKg
L7JRbLAaXbhZzjVlyIuhrxA3/+OcXcJJFzT/jCuLjfC8cSyTDB0FxLrHzarJXnzR
yQH3nAP2/Apd9Np75tt2QnDr9E0i2gB3b9bJXxf92nUupVcM9upctuBzpWjPoXTi
dYJ+EJ/B9aLrAek4sQpEzNPCifVJNYIKNLMc6YjCR06CDgo28EdPivEpBHXazeGa
XP9enZiVuppD0EqiFwUBBDDTMrOPAgMBAAGjgfgwgfUwDgYDVR0PAQH/BAQDAgGG
MB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATASBgNVHRMBAf8ECDAGAQH/
AgEAMB0GA1UdDgQWBBTnq58PLDOgU9NeT3jIsoQOO9aSMzAfBgNVHSMEGDAWgBR5
tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAKG
Fmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0gBAwwCjAIBgZngQwBAgEwJwYD
VR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVuY3Iub3JnLzANBgkqhkiG9w0B
AQsFAAOCAgEAUTdYUqEimzW7TbrOypLqCfL7VOwYf/Q79OH5cHLCZeggfQhDconl
k7Kgh8b0vi+/XuWu7CN8n/UPeg1vo3G+taXirrytthQinAHGwc/UdbOygJa9zuBc
VyqoH3CXTXDInT+8a+c3aEVMJ2St+pSn4ed+WkDp8ijsijvEyFwE47hulW0Ltzjg
9fOV5Pmrg/zxWbRuL+k0DBDHEJennCsAen7c35Pmx7jpmJ/HtgRhcnz0yjSBvyIw
6L1QIupkCv2SBODT/xDD3gfQQyKv6roV4G2EhfEyAsWpmojxjCUCGiyg97FvDtm/
NK2LSc9lybKxB73I2+P2G3CaWpvvpAiHCVu30jW8GCxKdfhsXtnIy2imskQqVZ2m
0Pmxobb28Tucr7xBK7CtwvPrb79os7u2XP3O5f9b/H66GNyRrglRXlrYjI1oGYL/
f4I1n/Sgusda6WvA6C190kxjU15Y12mHU4+BxyR9cx2hhGS9fAjMZKJss28qxvz6
Axu4CaDmRNZpK/pQrXF17yXCXkmEWgvSOEZy6Z9pcbLIVEGckV/iVeq0AOo2pkg9
p4QRIy0tK2diRENLSF2KysFwbY6B26BFeFs3v1sYVRhFW9nLkOrQVporCS0KyZmf
wVD89qSTlnctLcZnIavjKsKUu1nA1iU0yYMdYepKR7lWbnwhdx3ewok=
-----END CERTIFICATE-----

Thanks, your certificate is fine. The problem is with whatever is making the request having an outdated or incomplete ca bundle (list of trusted roots) and not knowing ISRG Root X1.

I’ve been using PhP’s curl function. And I found a fix:
Adding the line:
CURLOPT_SSL_VERIFYPEER => false,
in the PhP curl setup has made all the problems disappear. I’m not sure if it is actually ‘fixed’ or if I’m just covering up some unknown problem, but the net result is everything works even after changing the links back to https: instead of http:

Thank you for you help!

Using this option is essentially the same as telling your web browser to continue anyways when it says the validity of the site could not be verified. The remote site could be using a self-signed certificate and the client won’t care. The connection remains encrypted, but could be MitM attacked.

You should instead update the OS’s CA root store and make sure cURL is new enough and configured to use it -or- point cURL to an updated file you download yourself.

The authors of cURL themselves offer compatible CA root files from the same source as Mozilla Firefox: curl - Extract CA Certs from Mozilla

1 Like