在做同步指定时间的Garmin运动记录到不同账号的时候碰到一个诡异的报错,因为重装了系统就出现问题,让我觉得系统出现什么问题。

在编写python脚本的时候,需要提前获取一下Garmin账号密钥,于是执行下面的命令。

python run_page/get_garmin_secret.py 账号 "密码" --is-cn

报错啦。。。

Traceback (most recent call last):
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/urllib3/connectionpool.py", line 464, in _make_request
    self._validate_conn(conn)
    ~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/urllib3/connectionpool.py", line 1093, in _validate_conn
    conn.connect()
    ~~~~~~~~~~~~^^
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/urllib3/connection.py", line 790, in connect
    sock_and_verified = _ssl_wrap_socket_and_match_hostname(
        sock=sock,
    ...<14 lines>...
        assert_fingerprint=self.assert_fingerprint,
    )
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/urllib3/connection.py", line 969, in _ssl_wrap_socket_and_match_hostname
    ssl_sock = ssl_wrap_socket(
        sock=sock,
    ...<8 lines>...
        tls_in_tls=tls_in_tls,
    )
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/urllib3/util/ssl_.py", line 480, in ssl_wrap_socket
    ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls, server_hostname)
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/urllib3/util/ssl_.py", line 524, in _ssl_wrap_socket_impl
    return ssl_context.wrap_socket(sock, server_hostname=server_hostname)
           ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/[email protected]/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/ssl.py", line 455, in wrap_socket
    return self.sslsocket_class._create(
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
        sock=sock,
        ^^^^^^^^^^
    ...<5 lines>...
        session=session
        ^^^^^^^^^^^^^^^
    )
    ^
  File "/opt/homebrew/Cellar/[email protected]/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/ssl.py", line 1076, in _create
    self.do_handshake()
    ~~~~~~~~~~~~~~~~~^^
  File "/opt/homebrew/Cellar/[email protected]/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/ssl.py", line 1372, in do_handshake
    self._sslobj.do_handshake()
    ~~~~~~~~~~~~~~~~~~~~~~~~~^^
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1028)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/urllib3/connectionpool.py", line 787, in urlopen
    response = self._make_request(
        conn,
    ...<10 lines>...
        **response_kw,
    )
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/urllib3/connectionpool.py", line 488, in _make_request
    raise new_e
urllib3.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1028)

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/requests/adapters.py", line 667, in send
    resp = conn.urlopen(
        method=request.method,
    ...<9 lines>...
        chunked=chunked,
    )
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/urllib3/connectionpool.py", line 871, in urlopen
    return self.urlopen(
           ~~~~~~~~~~~~^
        method,
        ^^^^^^^
    ...<13 lines>...
        **response_kw,
        ^^^^^^^^^^^^^^
    )
    ^
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/urllib3/connectionpool.py", line 871, in urlopen
    return self.urlopen(
           ~~~~~~~~~~~~^
        method,
        ^^^^^^^
    ...<13 lines>...
        **response_kw,
        ^^^^^^^^^^^^^^
    )
    ^
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/urllib3/connectionpool.py", line 871, in urlopen
    return self.urlopen(
           ~~~~~~~~~~~~^
        method,
        ^^^^^^^
    ...<13 lines>...
        **response_kw,
        ^^^^^^^^^^^^^^
    )
    ^
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/urllib3/connectionpool.py", line 841, in urlopen
    retries = retries.increment(
        method, url, error=new_e, _pool=self, _stacktrace=sys.exc_info()[2]
    )
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/urllib3/util/retry.py", line 519, in increment
    raise MaxRetryError(_pool, url, reason) from reason  # type: ignore[arg-type]
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
urllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host='sso.garmin.cn', port=443): Max retries exceeded with url: /sso/embed?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.cn%2Fsso (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1028)')))                                                                                                                                                                      

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/xxx/PycharmProjects/workouts_page/run_page/get_garmin_secret.py", line 18, in <module>
    garth.login(options.email, options.password)
    ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/garth/http.py", line 167, in login
    self.oauth1_token, self.oauth2_token = sso.login(
                                           ~~~~~~~~~^
        *args, **kwargs, client=self
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/garth/sso.py", line 92, in login
    client.get("sso", "/sso/embed", params=SSO_EMBED_PARAMS)
    ~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/garth/http.py", line 155, in get
    return self.request("GET", *args, **kwargs)
           ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/garth/http.py", line 138, in request
    self.last_resp = self.sess.request(
                     ~~~~~~~~~~~~~~~~~^
        method,
        ^^^^^^^
    ...<3 lines>...
        **kwargs,
        ^^^^^^^^^
    )
    ^
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/requests/sessions.py", line 589, in request
    resp = self.send(prep, **send_kwargs)
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/requests/sessions.py", line 703, in send
    r = adapter.send(request, **kwargs)
  File "/Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/requests/adapters.py", line 698, in send
    raise SSLError(e, request=request)
requests.exceptions.SSLError: HTTPSConnectionPool(host='sso.garmin.cn', port=443): Max retries exceeded with url: /sso/embed?id=gauth-widget&embedWidget=true&gauthHost=https%3A%2F%2Fsso.garmin.cn%2Fsso (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1028)')))

问题扔给GPT,看看解决方法。

Round 1


GPT回复:

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1016)

这意味着Python 的 requests/urllib3 在访问 https://sso.garmin.cn 时,无法验证 SSL 证书,因为本地缺少根证书(CA 证书)。

按照提示检查 requests/certifi 版本和检查系统时间,都是正常。

pip install --upgrade requests certifi urllib3

Round 2

于是再次告诉GPT检查正常,后面怎么处理。

GPT回复:
你的 certifirequestsurllib3 都是最新版,依然出现证书校验失败,说明不是包的版本问题,而是本地 Python 环境的 CA 证书链没有被正确加载

1. 检查 CA 证书路径(核心)

在你的 Python 虚拟环境中,运行以下代码,看看 certifi 的 CA 文件路径:

import certifi
print(certifi.where())

输出的路径应该是类似:

/Users/xxx/PycharmProjects/workouts_page/venv311/lib/python3.11/site-packages/certifi/cacert.pem

2. 检查 requests 是否用到了正确的 CA 文件

再运行:

import requests
print(requests.certs.where())

输出应该和上面一致。

3. 手动测试 HTTPS 请求

在虚拟环境下,运行:

import requests
print(requests.get('https://www.baidu.com').status_code)

如果这也报 SSL 错,说明你的 Python 环境根本无法校验证书

4. 强制指定 CA 证书(临时测试)

你可以试试这样写:

import requests
import certifi

r = requests.get('https://sso.garmin.cn', verify=certifi.where())
print(r.status_code)

如果这样能成功,说明就是证书链没被自动加载。

第1、2、3都能正常运行,第4输出类似上面的报错


Round 3

于是把结果信息和报错给GPT,接着处理。

你的测试结果非常关键:

  • 访问 https://www.baidu.com 没问题(200)。
  • 访问 https://sso.garmin.cn 依然报证书校验失败,即使用的是 certifi.where()

这说明:

  • 你的 Python 环境和 certifi 没问题(否则百度也会失败)。
  • 问题很可能出在 Garmin 的证书链不被 certifi 信任,或者 Garmin 的证书链在中国大陆被污染/不完整

1. 手动检查 Garmin 证书链

你可以用 openssl 检查 Garmin 的证书链:

openssl s_client -connect sso.garmin.cn:443 -showcerts -CAfile /Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/certifi/cacert.pem

看下输出里的证书链是否完整,以及是否有 “Verify return code: 0 (ok)”。

在执行以上提示检查,有如下输出:

Connecting to 60.9.5.109
CONNECTED(00000005)
depth=2 C=US, O=SSL Corporation, CN=SSL.com TLS Transit ECC CA R2
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=1 C=US, O=CLOUDFLARE, INC., CN=Cloudflare TLS Issuing ECC CA 1
verify return:1
depth=0 CN=sso.garmin.cn
verify return:1
---
Certificate chain
 0 s:CN=sso.garmin.cn
   i:C=US, O=CLOUDFLARE, INC., CN=Cloudflare TLS Issuing ECC CA 1
   a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256
   v:NotBefore: May 30 20:35:58 2025 GMT; NotAfter: Aug 28 20:41:18 2025 GMT
-----BEGIN CERTIFICATE-----
MIID4zCCA4mgAwIBAgIQYr/luWpLXqfeS6LtCXrAcTAKBggqhkjOPQQDAjBSMQsw
CQYDVQQGEwJVUzEZMBcGA1UECgwQQ0xPVURGTEFSRSwgSU5DLjEoMCYGA1UEAwwf
Q2xvdWRmbGFyZSBUTFMgSXNzdWluZyBFQ0MgQ0EgMTAeFw0yNTA1MzAyMDM1NTha
Fw0yNTA4MjgyMDQxMThaMBgxFjAUBgNVBAMMDXNzby5nYXJtaW4uY24wWTATBgcq
hkjOPQIBBggqhkjOPQMBBwNCAAQHMqqkpF8gWRUi70UdkQOGg1vVvaNTWJQVAy1R
6gsAi2QtTLWg8DbIVFcLKroKB6td9KLy1SQQbTvbaF9fdU4no4ICeTCCAnUwDAYD
VR0TAQH/BAIwADAfBgNVHSMEGDAWgBScxAlyRxgXe6caibOSNdXhA4z+kjBsBggr
BgEFBQcBAQRgMF4wOQYIKwYBBQUHMAKGLWh0dHA6Ly9pLmNmLWIuc3NsLmNvbS9D
bG91ZGZsYXJlLVRMUy1JLUUxLmNlcjAhBggrBgEFBQcwAYYVaHR0cDovL28uY2Yt
Yi5zc2wuY29tMCkGA1UdEQQiMCCCDXNzby5nYXJtaW4uY26CDyouc3NvLmdhcm1p
bi5jbjAjBgNVHSAEHDAaMAgGBmeBDAECATAOBgwrBgEEAYKpMAEDAQEwHQYDVR0l
BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6
Ly9jLmNmLWIuc3NsLmNvbS9DbG91ZGZsYXJlLVRMUy1JLUUxLmNybDAOBgNVHQ8B
Af8EBAMCB4AwDwYJKwYBBAGC2kssBAIFADCCAQQGCisGAQQB1nkCBAIEgfUEgfIA
8AB3AO08S9boBsKkogBX28sk4jgB31Ev7cSGxXAPIN23Pj/gAAABlyLxxpUAAAQD
AEgwRgIhANSfg4eJjx1RKc0zG3yN54SpDBvpOx46Jrafvv5HPmqvAiEA7a5n5u00
K7ijQIhTytgqGqdeK7WVbhYbx4I19SAc2fAAdQDM+w9qhXEJZf6Vm1PO6bJ8IumF
XA2XjbapflTA/kwNsAAAAZci8catAAAEAwBGMEQCIBfkAwShefUxMPkEaHyc/msz
sPSFFr9UOajTpSn+7Z3XAiBO3rvSeq3kbv+PDiWXX+QD8qp27PTy5Uz4601Z7pIO
kTAKBggqhkjOPQQDAgNIADBFAiBH6Tk0U4UuJtKzBkqGPqSjldg3DPRIXNcd5OR/
Ju8LSQIhAP3fpefdqffqldZ+wvLnxaNgMjYy6GyNJCP/P29PbtcI
-----END CERTIFICATE-----
 1 s:C=US, O=CLOUDFLARE, INC., CN=Cloudflare TLS Issuing ECC CA 1
   i:C=US, O=SSL Corporation, CN=SSL.com TLS Transit ECC CA R2
   a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA384
   v:NotBefore: Oct 31 17:17:49 2023 GMT; NotAfter: Oct 28 17:17:48 2033 GMT
-----BEGIN CERTIFICATE-----
MIIC5DCCAmqgAwIBAgIQLD+iaS9BE707f+W2BLSdTTAKBggqhkjOPQQDAzBPMQsw
CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSYwJAYDVQQDDB1T
U0wuY29tIFRMUyBUcmFuc2l0IEVDQyBDQSBSMjAeFw0yMzEwMzExNzE3NDlaFw0z
MzEwMjgxNzE3NDhaMFIxCzAJBgNVBAYTAlVTMRkwFwYDVQQKDBBDTE9VREZMQVJF
LCBJTkMuMSgwJgYDVQQDDB9DbG91ZGZsYXJlIFRMUyBJc3N1aW5nIEVDQyBDQSAx
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEByHHIHytNSzTS+F3JA7hHMDGd2cp
cY9i3MLTKmE6DJTKc6JwvW50pwKodvd2Qj4RAAy2jSejsVgw5jeh6syt3KOCASMw
ggEfMBIGA1UdEwEB/wQIMAYBAf8CAQAwHwYDVR0jBBgwFoAUMqLH2FiL/3/APPJV
aTPszswfvJcwSAYIKwYBBQUHAQEEPDA6MDgGCCsGAQUFBzAChixodHRwOi8vY2Vy
dC5zc2wuY29tL1NTTC5jb20tVExTLVQtRUNDLVIyLmNlcjARBgNVHSAECjAIMAYG
BFUdIAAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMD0GA1UdHwQ2MDQw
MqAwoC6GLGh0dHA6Ly9jcmxzLnNzbC5jb20vU1NMLmNvbS1UTFMtVC1FQ0MtUjIu
Y3JsMB0GA1UdDgQWBBScxAlyRxgXe6caibOSNdXhA4z+kjAOBgNVHQ8BAf8EBAMC
AYYwCgYIKoZIzj0EAwMDaAAwZQIxAL0Sk3RweR6uG1aSHF3JgHQptubP9xoZyUmz
HSa+SSdY5wTGSx5qAowrLPCpLio2PAIwXQGgYzf5QzD/1Bsu87WrUcIVtLixr5KQ
wKBaFAyIJ7OOiWgW0HV/NA1UeuSe0zmN
-----END CERTIFICATE-----
 2 s:C=US, O=SSL Corporation, CN=SSL.com TLS Transit ECC CA R2
   i:C=GB, ST=Greater Manchester, L=Salford, O=Comodo CA Limited, CN=AAA Certificate Services
   a:PKEY: EC, (secp384r1); sigalg: sha256WithRSAEncryption
   v:NotBefore: Jun 21 00:00:00 2024 GMT; NotAfter: Dec 31 23:59:59 2028 GMT
-----BEGIN CERTIFICATE-----
MIID0DCCArigAwIBAgIRAK2NLfZGgaDTZEfqqU+ic8EwDQYJKoZIhvcNAQELBQAw
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNDA2MjEwMDAwMDBaFw0y
ODEyMzEyMzU5NTlaME8xCzAJBgNVBAYTAlVTMRgwFgYDVQQKDA9TU0wgQ29ycG9y
YXRpb24xJjAkBgNVBAMMHVNTTC5jb20gVExTIFRyYW5zaXQgRUNDIENBIFIyMHYw
EAYHKoZIzj0CAQYFK4EEACIDYgAEZOd9mQNTXJEe6vjYI62hvyziY4nvKGj27dfw
7Ktorncr5HaXG1Dr21koLW+4NrmrjZfKTCKe7onZAj/9enM6kI0rzC86N4PaDbQt
RRtzcgllX3ghPeeLZj9H/Qkp1hQPo4IBJzCCASMwHwYDVR0jBBgwFoAUoBEKIz6W
8Qfs4q8p74Klf9AwpLQwHQYDVR0OBBYEFDKix9hYi/9/wDzyVWkz7M7MH7yXMA4G
A1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdJQQWMBQGCCsG
AQUFBwMBBggrBgEFBQcDAjAjBgNVHSAEHDAaMAgGBmeBDAECATAOBgwrBgEEAYKp
MAEDAQEwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v
QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQGCCsG
AQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQELBQAD
ggEBAB4oL4ChKaKGZVZK8uAXjj8wvFdm45uvhU/t14QeH5bwETeKiQQXBga4/Nyz
zvpfuoEycantX+tHl/muwpmuHT0Z6IKYoICaMxOIktcTF4qHvxQW2WItHjOglrTj
qlXJXVL+3HCO60TEloSX8eUGsqfLQkc//z3Lb4gz117+fkDbnPt8+2REq3SCvaAG
hlh/lWWfHqTAiHed/qqzBSYqqvfjNlhIfXnPnhfAv/PpOUO1PmxCEAEYrg+VoS+O
+EBd1zkT0V7CfrPpj30cAMs2h+k4pPMwcLuB3Ku4TncBTRyt5K0gbJ3pQ0Rk9Hmu
wOz5QAZ+2n1q4TlApJzBfwFrCDg=
-----END CERTIFICATE-----
---
Server certificate
subject=CN=sso.garmin.cn
issuer=C=US, O=CLOUDFLARE, INC., CN=Cloudflare TLS Issuing ECC CA 1
---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: ecdsa_secp256r1_sha256
Negotiated TLS1.3 group: X25519MLKEM768
---
SSL handshake has read 4130 bytes and written 1626 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 256 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)

Round 4

把输出结果再次给GPT,进一步分析

你的 openssl s_client 输出非常有价值,可以确认问题点:

1. 证书链分析

你拿到的完整证书链如下:

  1. 站点证书:CN=sso.garmin.cn

    • 颁发者:Cloudflare TLS Issuing ECC CA 1
  2. 中间证书:CN=Cloudflare TLS Issuing ECC CA 1

    • 颁发者:SSL.com TLS Transit ECC CA R2
  3. 根证书:CN=SSL.com TLS Transit ECC CA R2

    • 颁发者:AAA Certificate Services(Comodo CA Limited)

2. openssl 校验失败的原因

verify error:num=20:unable to get local issuer certificate

这说明本地缺少信任链中的某个根证书
通常是SSL.com TLS Transit ECC CA R2这个根证书没有被 certifi 收录或 certifi 版本太新/太旧。

3. certifi 是否包含这个根证书?

你可以这样查找:

grep "SSL.com TLS Transit ECC CA R2" $(python -c "import certifi; print(certifi.where())")

如果没有输出,说明 certifi 没有收录这个根证书。

4. 解决方案

A. 手动追加根证书到 certifi

  1. 复制 openssl 输出中的

    -----BEGIN CERTIFICATE-----
    ...(SSL.com TLS Transit ECC CA R2的内容)...
    -----END CERTIFICATE-----
    

    也就是最后一个证书块depth=2 那个)。

  2. 追加到 certifi.where() 路径下的 cacert.pem 文件末尾

  3. 再试你的 requests 代码

于是到https://www.ssl.com/repository/找到对应的PEM放到cacert.pem文件末尾,运行获取Garmin密钥的脚本,还是报错。

Round 5

再来~

你的分析和操作都非常细致,但问题依然没有解决,说明有几个点需要进一步排查。我们来一步步定位:

1. 你追加的根证书内容没问题

你追加的这段证书确实就是 SSL.com TLS Transit ECC CA R2。这一步是对的

2. 为什么还是出现“unable to get issuer certificate”?

可能的原因有:

  1. 追加位置不对
    证书必须追加到 cacert.pem 文件的末尾,且不能有多余的空格或字符。

  2. 格式不对
    PEM 格式必须严格正确,不能有多余的换行或空格。

  3. Python、requests、certifi实际用到的文件不是你改的那个
    你可以用如下命令确认requests实际用到的 cacert.pem 路径

    import certifi
    print(certifi.where())
    

    你已经做了这一步,路径是

    /Users/xxx/PycharmProjects/workouts_page/venv311/lib/python3.11/site-packages/certifi/cacert.pem
    

    你确认就是改的这个文件吗?(有多个虚拟环境时很容易搞错)

  4. 链条还缺上一级根? 你的 SSL.com TLS Transit ECC CA R2 的颁发者是 AAA Certificate Services,而不是自签名。这意味着它不是最顶级根证书,还需要再加上 AAA Certificate Services 的根证书!

3. 解决办法

检查是否还需要追加“AAA Certificate Services”根证书

你的 SSL.com TLS Transit ECC CA R2 的颁发者是:

CN=AAA Certificate Services

这说明它不是自签名根证书,还需要上一层的根证书才完整。

于是把AAA Certificate Services放到 cacert.pem 文件末尾。

到此终于可以正常获取Garmin Connect账号的密钥了。

再看看openssl 检查 Garmin 的证书链:

openssl s_client -connect sso.garmin.cn:443 -showcerts -CAfile /Users/xxx/PycharmProjects/workouts_page/venv313/lib/python3.13/site-packages/certifi/cacert.pem

正常的结果是这样的:

Connecting to 60.9.5.109
CONNECTED(00000005)
depth=3 C=GB, ST=Greater Manchester, L=Salford, O=Comodo CA Limited, CN=AAA Certificate Services
verify return:1
depth=2 C=US, O=SSL Corporation, CN=SSL.com TLS Transit ECC CA R2
verify return:1
depth=1 C=US, O=CLOUDFLARE, INC., CN=Cloudflare TLS Issuing ECC CA 1
verify return:1
depth=0 CN=sso.garmin.cn
verify return:1
---
Certificate chain
 0 s:CN=sso.garmin.cn
   i:C=US, O=CLOUDFLARE, INC., CN=Cloudflare TLS Issuing ECC CA 1
   a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA256
   v:NotBefore: May 30 20:35:58 2025 GMT; NotAfter: Aug 28 20:41:18 2025 GMT
-----BEGIN CERTIFICATE-----
MIID4zCCA4mgAwIBAgIQYr/luWpLXqfeS6LtCXrAcTAKBggqhkjOPQQDAjBSMQsw
CQYDVQQGEwJVUzEZMBcGA1UECgwQQ0xPVURGTEFSRSwgSU5DLjEoMCYGA1UEAwwf
Q2xvdWRmbGFyZSBUTFMgSXNzdWluZyBFQ0MgQ0EgMTAeFw0yNTA1MzAyMDM1NTha
Fw0yNTA4MjgyMDQxMThaMBgxFjAUBgNVBAMMDXNzby5nYXJtaW4uY24wWTATBgcq
hkjOPQIBBggqhkjOPQMBBwNCAAQHMqqkpF8gWRUi70UdkQOGg1vVvaNTWJQVAy1R
6gsAi2QtTLWg8DbIVFcLKroKB6td9KLy1SQQbTvbaF9fdU4no4ICeTCCAnUwDAYD
VR0TAQH/BAIwADAfBgNVHSMEGDAWgBScxAlyRxgXe6caibOSNdXhA4z+kjBsBggr
BgEFBQcBAQRgMF4wOQYIKwYBBQUHMAKGLWh0dHA6Ly9pLmNmLWIuc3NsLmNvbS9D
bG91ZGZsYXJlLVRMUy1JLUUxLmNlcjAhBggrBgEFBQcwAYYVaHR0cDovL28uY2Yt
Yi5zc2wuY29tMCkGA1UdEQQiMCCCDXNzby5nYXJtaW4uY26CDyouc3NvLmdhcm1p
bi5jbjAjBgNVHSAEHDAaMAgGBmeBDAECATAOBgwrBgEEAYKpMAEDAQEwHQYDVR0l
BBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6
Ly9jLmNmLWIuc3NsLmNvbS9DbG91ZGZsYXJlLVRMUy1JLUUxLmNybDAOBgNVHQ8B
Af8EBAMCB4AwDwYJKwYBBAGC2kssBAIFADCCAQQGCisGAQQB1nkCBAIEgfUEgfIA
8AB3AO08S9boBsKkogBX28sk4jgB31Ev7cSGxXAPIN23Pj/gAAABlyLxxpUAAAQD
AEgwRgIhANSfg4eJjx1RKc0zG3yN54SpDBvpOx46Jrafvv5HPmqvAiEA7a5n5u00
K7ijQIhTytgqGqdeK7WVbhYbx4I19SAc2fAAdQDM+w9qhXEJZf6Vm1PO6bJ8IumF
XA2XjbapflTA/kwNsAAAAZci8catAAAEAwBGMEQCIBfkAwShefUxMPkEaHyc/msz
sPSFFr9UOajTpSn+7Z3XAiBO3rvSeq3kbv+PDiWXX+QD8qp27PTy5Uz4601Z7pIO
kTAKBggqhkjOPQQDAgNIADBFAiBH6Tk0U4UuJtKzBkqGPqSjldg3DPRIXNcd5OR/
Ju8LSQIhAP3fpefdqffqldZ+wvLnxaNgMjYy6GyNJCP/P29PbtcI
-----END CERTIFICATE-----
 1 s:C=US, O=CLOUDFLARE, INC., CN=Cloudflare TLS Issuing ECC CA 1
   i:C=US, O=SSL Corporation, CN=SSL.com TLS Transit ECC CA R2
   a:PKEY: EC, (prime256v1); sigalg: ecdsa-with-SHA384
   v:NotBefore: Oct 31 17:17:49 2023 GMT; NotAfter: Oct 28 17:17:48 2033 GMT
-----BEGIN CERTIFICATE-----
MIIC5DCCAmqgAwIBAgIQLD+iaS9BE707f+W2BLSdTTAKBggqhkjOPQQDAzBPMQsw
CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSYwJAYDVQQDDB1T
U0wuY29tIFRMUyBUcmFuc2l0IEVDQyBDQSBSMjAeFw0yMzEwMzExNzE3NDlaFw0z
MzEwMjgxNzE3NDhaMFIxCzAJBgNVBAYTAlVTMRkwFwYDVQQKDBBDTE9VREZMQVJF
LCBJTkMuMSgwJgYDVQQDDB9DbG91ZGZsYXJlIFRMUyBJc3N1aW5nIEVDQyBDQSAx
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEByHHIHytNSzTS+F3JA7hHMDGd2cp
cY9i3MLTKmE6DJTKc6JwvW50pwKodvd2Qj4RAAy2jSejsVgw5jeh6syt3KOCASMw
ggEfMBIGA1UdEwEB/wQIMAYBAf8CAQAwHwYDVR0jBBgwFoAUMqLH2FiL/3/APPJV
aTPszswfvJcwSAYIKwYBBQUHAQEEPDA6MDgGCCsGAQUFBzAChixodHRwOi8vY2Vy
dC5zc2wuY29tL1NTTC5jb20tVExTLVQtRUNDLVIyLmNlcjARBgNVHSAECjAIMAYG
BFUdIAAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMD0GA1UdHwQ2MDQw
MqAwoC6GLGh0dHA6Ly9jcmxzLnNzbC5jb20vU1NMLmNvbS1UTFMtVC1FQ0MtUjIu
Y3JsMB0GA1UdDgQWBBScxAlyRxgXe6caibOSNdXhA4z+kjAOBgNVHQ8BAf8EBAMC
AYYwCgYIKoZIzj0EAwMDaAAwZQIxAL0Sk3RweR6uG1aSHF3JgHQptubP9xoZyUmz
HSa+SSdY5wTGSx5qAowrLPCpLio2PAIwXQGgYzf5QzD/1Bsu87WrUcIVtLixr5KQ
wKBaFAyIJ7OOiWgW0HV/NA1UeuSe0zmN
-----END CERTIFICATE-----
 2 s:C=US, O=SSL Corporation, CN=SSL.com TLS Transit ECC CA R2
   i:C=GB, ST=Greater Manchester, L=Salford, O=Comodo CA Limited, CN=AAA Certificate Services
   a:PKEY: EC, (secp384r1); sigalg: sha256WithRSAEncryption
   v:NotBefore: Jun 21 00:00:00 2024 GMT; NotAfter: Dec 31 23:59:59 2028 GMT
-----BEGIN CERTIFICATE-----
MIID0DCCArigAwIBAgIRAK2NLfZGgaDTZEfqqU+ic8EwDQYJKoZIhvcNAQELBQAw
ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yNDA2MjEwMDAwMDBaFw0y
ODEyMzEyMzU5NTlaME8xCzAJBgNVBAYTAlVTMRgwFgYDVQQKDA9TU0wgQ29ycG9y
YXRpb24xJjAkBgNVBAMMHVNTTC5jb20gVExTIFRyYW5zaXQgRUNDIENBIFIyMHYw
EAYHKoZIzj0CAQYFK4EEACIDYgAEZOd9mQNTXJEe6vjYI62hvyziY4nvKGj27dfw
7Ktorncr5HaXG1Dr21koLW+4NrmrjZfKTCKe7onZAj/9enM6kI0rzC86N4PaDbQt
RRtzcgllX3ghPeeLZj9H/Qkp1hQPo4IBJzCCASMwHwYDVR0jBBgwFoAUoBEKIz6W
8Qfs4q8p74Klf9AwpLQwHQYDVR0OBBYEFDKix9hYi/9/wDzyVWkz7M7MH7yXMA4G
A1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdJQQWMBQGCCsG
AQUFBwMBBggrBgEFBQcDAjAjBgNVHSAEHDAaMAgGBmeBDAECATAOBgwrBgEEAYKp
MAEDAQEwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v
QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQGCCsG
AQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQELBQAD
ggEBAB4oL4ChKaKGZVZK8uAXjj8wvFdm45uvhU/t14QeH5bwETeKiQQXBga4/Nyz
zvpfuoEycantX+tHl/muwpmuHT0Z6IKYoICaMxOIktcTF4qHvxQW2WItHjOglrTj
qlXJXVL+3HCO60TEloSX8eUGsqfLQkc//z3Lb4gz117+fkDbnPt8+2REq3SCvaAG
hlh/lWWfHqTAiHed/qqzBSYqqvfjNlhIfXnPnhfAv/PpOUO1PmxCEAEYrg+VoS+O
+EBd1zkT0V7CfrPpj30cAMs2h+k4pPMwcLuB3Ku4TncBTRyt5K0gbJ3pQ0Rk9Hmu
wOz5QAZ+2n1q4TlApJzBfwFrCDg=
-----END CERTIFICATE-----
---
Server certificate
subject=CN=sso.garmin.cn
issuer=C=US, O=CLOUDFLARE, INC., CN=Cloudflare TLS Issuing ECC CA 1
---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: ecdsa_secp256r1_sha256
Negotiated TLS1.3 group: X25519MLKEM768
---
SSL handshake has read 4130 bytes and written 1626 bytes
Verification: OK
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Protocol: TLSv1.3
Server public key is 256 bit
This TLS version forbids renegotiation.
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)