EG21-G: Client certificate not presented during TLS handshake at seclevel=0?

Module: EG21-G
Firmware version: (EG21GGBR07A11M1G_01.002.01.002)

Issue:
I’m configuring the EG21-G to present a client certificate during an HTTPS request (mTLS-style client authentication), but I have evidence suggesting the client certificate is not actually being sent during the TLS handshake at seclevel=0, despite clientcert/clientkey being correctly configured.

Context:
My target server validates client certificates when one is presented (confirmed independently via Postman: attaching a correct client cert succeeds, attaching a mismatched/wrong client cert is rejected by the server). So the server side is known to behave correctly and consistently.

Test sequence (AT commands):

AT+QICSGP=1,1,“”,“”,“”,1
AT+QIACT=1
AT+QIACT?
+QIACT: 1,1,1,“”

AT+QFDEL=“UFS:cert.pem”
AT+QFDEL=“UFS:key.pem”
AT+QFUPL=“UFS:cert.pem”,570,30
CONNECT
<570 bytes of valid client cert.pem>
+QFUPL: 570,547f
OK
AT+QFUPL=“UFS:key.pem”,227,30
CONNECT
<227 bytes of matching client key.pem>
+QFUPL: 227,6272
OK

AT+QSSLCFG=“sslversion”,1,4
AT+QSSLCFG=“seclevel”,1,0
AT+QSSLCFG=“clientcert”,1,“UFS:cert.pem”
AT+QSSLCFG=“clientkey”,1,“UFS:key.pem”
AT+QSSLCFG=“ignorelocaltime”,1,1

AT+QHTTPCFG=“contextid”,1
AT+QHTTPCFG=“sslctxid”,1
AT+QHTTPCFG=“requestheader”,0
AT+QHTTPCFG=“contenttype”,4

AT+QHTTPURL=49,60
CONNECT

OK
AT+QHTTPGET=80
OK
+QHTTPGET: 0,200

The test that demonstrates the problem:

  1. Ran the above with my correct, valid 570-byte client certificate → +QHTTPGET: 0,200 (success).

  2. Deleted the cert, uploaded a different, deliberately mismatched 240-byte file as cert.pem (confirmed genuinely different via the +QFUPL checksum changing from 547f to 237e — not a re-upload of the same file), reconfigured clientcert to point at it, and re-sent the same request to the same URL → still got +QHTTPGET: 0,200 (success).

Since the server is independently confirmed (via Postman) to reject mismatched client certificates when one is actually presented and checked, getting an identical success result regardless of which certificate is loaded onto the modem suggests the certificate configured via AT+QSSLCFG="clientcert"/"clientkey" is not actually being transmitted during the EG21-G’s TLS handshake when seclevel=0.

Questions:

  1. Is the client certificate expected to be sent during the handshake when seclevel=0, or does seclevel=0 suppress client certificate presentation as well as server certificate verification?

  2. If client cert presentation should work at seclevel=0, is there an additional required step (e.g., explicit AT+QSSLOPEN/socket-level handling) that the AT+QHTTPGET/AT+QHTTPPOST high-level commands don’t perform automatically?

  3. Is seclevel=2 (combined with a CA cert for server verification, which I can’t currently provide due to this server’s ECDSA chain) the only way to guarantee client cert presentation on this module?

Any clarification on the actual handshake behavior at each seclevel would be very helpful. Happy to provide full AT command logs (with sensitive cert data redacted) if useful for debugging.

Dear Customer,

Thanks for the detailed test sequence and the Postman cross-check that comparison made the root cause clear, and your diagnosis is correct. The client certificate is not being transmitted during the TLS handshake, and this is expected behavior at seclevel=0 rather than a fault.

On the EG21-G, the seclevel value in AT+QSSLCFG sets the authentication mode for the whole handshake, not just server-side verification:

0 - No authentication
1 - Perform server authentication
2 - Perform server and client authentication (client cert presented if the server requests it)

At seclevel=0 the module performs no authentication in either direction. The clientcert/clientkey paths you configure are stored in the SSL context but are never used during the handshake so even when the server sends a CertificateRequest, the module presents nothing. This is exactly why swapping your valid 570-byte cert for the mismatched 240-byte file produced an identical 200: in both cases no client cert reached the wire, so the request succeeded on the server’s “no cert presented” path. The checksum change you saw confirms the file on UFS was updated, but seclevel=0 gates whether it is ever sent.

To answer your specific questions:

  1. The client certificate is not expected to be sent at seclevel=0. That level suppresses both server verification and client cert presentation.

  2. There is no additional step that would make seclevel=0 present a client cert. AT+QHTTPGET/AT+QHTTPPOST apply whatever seclevel is set on the SSL context (via AT+QHTTPCFG=“sslctxid”), and dropping to raw AT+QSSLOPEN would behave the same way — client cert presentation is governed entirely by seclevel.

  3. seclevel=2 is the correct way to force client cert presentation. We understand your blocker is that seclevel 1 and 2 normally require a trusted CA certificate for server verification, which you can’t supply due to the server’s ECDSA chain. You can work around this with AT+QSSLCFG=“ignorecertitem”, which relaxes or bypasses the server-side checks while keeping seclevel=2 so the client cert is still presented. Relevant bit values:

8 - ignore that the cert is not signed by the trusted CA
32768 - ignore an unacceptable public key algorithm (explicitly includes ECDSA)
1048575 - ignore all check items (no server cert checking at all)

Suggested test configuration:

AT+QSSLCFG=“seclevel”,1,2
AT+QSSLCFG=“clientcert”,1,“UFS:cert.pem”
AT+QSSLCFG=“clientkey”,1,“UFS:key.pem”
AT+QSSLCFG=“ignorecertitem”,1,1048575

Start with 1048575 to confirm client cert presentation works end to end, then tighten to the minimum bitmask you need for production (e.g. 8 or 8+32768).

Two points worth verifying on your hardware before you finalize:

  • ignorecertitem takes effect immediately but is not saved, so re-apply it each session alongside the rest of the context config.
  • If seclevel=2 errors out with no cacert configured at all, upload any placeholder cert as cacert purely to satisfy the config requirement and let ignorecertitem perform the actual bypass.

As a final validation, please repeat your differential test under this configuration: the correct cert should return 200, and the deliberately mismatched cert should now be rejected by the server. That reproduces your Postman result on the module and confirms the certificate is genuinely on the wire.

If you can share the redacted handshake logs from that run, I’m happy to confirm the cert is being presented as expected.

Best regards,
Fazrul Redzuan

Hi Fazrul,
Thanks for confirming the seclevel=0 behavior — that matches what I found. Following your suggested config, I tried moving to seclevel=2 and ran into a separate, more fundamental issue: AT+QSSLOPEN fails with error 552 (invalid parameter) on every attempt at seclevel=1 or seclevel=2, regardless of target server or certificate.

Full reproduction, same module/firmware as my original post (EG21-G, EG21GGBR07A11M1G_01.002.01.002):

  • AT+QSSLOPEN with seclevel=0 succeeds reliably against any target.

  • AT+QSSLOPEN with seclevel=1 or seclevel=2 returns +QSSLOPEN: ,552 on every attempt, including against www.google.com:443 (public root CA, not my own server) and against my own Let’s Encrypt-secured host.

  • Ruled out cacert content: tested both a single root cert (ISRG Root X1, 1939 bytes) and a full root+intermediate chain (3505 bytes). Read both back via AT+QFOPEN/AT+QFREAD and confirmed byte-identical to the source files — no corruption.

  • Ruled out clock/time: AT+CCLK? initially showed 1980 (never synced). Fixed via AT+QNTP, confirmed correct date afterward. Also set AT+QSSLCFG=“ignorelocaltime”,1,1. 552 persisted.
    Ruled out relaxed validation flags: set both AT+QSSLCFG=“ignoreinvalidcertsign”,1,1 and AT+QSSLCFG=“ignoremulticertchainverify”,1,1. 552 persisted.

  • Ruled out DNS/APN/network layer: confirmed working PDP context, correct APN, successful AT+QPING to raw IPs and hostnames, and successful AT+QSSLOPEN at seclevel=0 to the exact same target IP that fails at seclevel=1/2.

This points to the seclevel 1/2 validation path itself failing universally in this build, separate from the client-cert-presentation issue in my original post. Given we’re on identical firmware, wanted to flag this in case it’s the same underlying root cause, or a related regression.

Is there a known issue with seclevel 1/2 in EG21GGBR07A11M1G_01.002.01.002, and is there a newer firmware build available that addresses this? Happy to share full AT command logs if useful.

Thanks,

Muhammad Kashif

Hi Kashif,

Thanks for the detailed write-up the ruling-out work is thorough, especially confirming the cert files byte-for-byte and isolating seclevel 0 success against the same target IP.

Before we treat this as a firmware issue, I’d like to check one layer the steps so far don’t quite cover, because the error code points somewhere specific.

Error 552 is “invalid parameter,” and the important detail is when it’s raised: it’s a pre-flight check that fires before the SSL handshake even starts. If the problem were in the seclevel 1/2 certificate validation itself chain, validity, signature the handshake would actually run and then fail, and the result would come back in the +QSSLOPEN: , URC after negotiation, with a different code. An immediate +QSSLOPEN: ,552 with an empty clientID means the module rejected the request before allocating the socket. That’s a configuration check failing, not the validation path.

For seclevel 1/2, the module requires a CA certificate that is both (a) uploaded to the filesystem and (b) bound to the specific SSL context ID you pass to QSSLOPEN. Reading the file back with QFOPEN/QFREAD confirms (a), but not (b) it doesn’t tell us the cert is bound to the right context, that the path prefix matches where the file actually lives, or that seclevel is set on the same context ID you’re opening.

Could you run these three queries and share the output? They take a few seconds and will show exactly where 552 is coming from:

  1. AT+QSSLCFG=“cacert”, (query form) — does it return the cert path, or empty?
  2. AT+QSSLCFG=“seclevel”, — confirm 1 or 2 is set on that exact context.
  3. Confirm the cacert path prefix. Files uploaded with QFUPL go to RAM: by default; if the cert is at UFS: but bound as RAM: (or named differently), that mismatch alone returns 552.

Also helpful: your exact QSSLCFG cacert / seclevel set commands and the full QSSLOPEN line, so we can confirm the context IDs line up across all three.

The fact that google.com:443 with a public root fails the same way actually supports this if the context is misconfigured, every open fails identically regardless of target.

If the readback shows the cacert correctly bound to the same context ID, with the right path and seclevel set, and it still returns 552 then we have something worth digging into further, and I’ll take it from there. I will send the latest firmware if the issue still persists.

Hi Fazrul,

Following your guidance, I verified all three points carefully after a full modem reset and clean reconfiguration. Below are the complete AT command logs.

Full configuration sequence after fresh reset:

AT+QFLST
+QFLST: “clientkey.pem”,227
+QFLST: “cacert.pem”,3505
+QFLST: “clientcert.pem”,570
OK

AT+QICSGP=1,1,“telenor”,“”,“”,1
OK

AT+QIACT=1
OK

AT+QIACT?
+QIACT: 1,1,1,“10.85.140.96”
OK

AT+QNTP=1,“pool.ntp.org
OK
+QNTP: 0,“2026/07/01,04:51:21+00”

AT+CCLK?
+CCLK: “26/07/01,04:51:28”
OK

AT+QSSLCFG=“cacert”,1,“cacert.pem”
OK
AT+QSSLCFG=“clientcert”,1,“clientcert.pem”
OK
AT+QSSLCFG=“clientkey”,1,“clientkey.pem”,“”
OK
AT+QSSLCFG=“seclevel”,1,2
OK
AT+QSSLCFG=“sslversion”,1,4
OK
AT+QSSLCFG=“ciphersuite”,1,0xFFFF
OK
AT+QSSLCFG=“ignorelocaltime”,1,1
OK
AT+QSSLCFG=“ignoreinvalidcertsign”,1,1
OK
AT+QSSLCFG=“ignoremulticertchainverify”,1,1
OK

Verification queries — all bindings confirmed non-empty:

AT+QSSLCFG=“cacert”,1
+QSSLCFG: “cacert”,1,“cacert.pem”
OK

AT+QSSLCFG=“clientcert”,1
+QSSLCFG: “clientcert”,1,“clientcert.pem”
OK

AT+QSSLCFG=“clientkey”,1
+QSSLCFG: “clientkey”,1,“clientkey.pem”
OK

AT+QSSLCFG=“seclevel”,1
+QSSLCFG: “seclevel”,1,2
OK

AT+QSSLCFG=“sslversion”,1
+QSSLCFG: “sslversion”,1,4
OK

Path prefix testing — all three variants tested, all return 552:

AT+QSSLCFG=“cacert”,1,“cacert.pem”
OK
AT+QSSLOPEN=1,1,0,“143.244.223.96”,443
OK
+QSSLOPEN: 0,552

AT+QSSLCFG=“cacert”,1,“RAM:cacert.pem”
AT+QSSLCFG=“clientcert”,1,“RAM:clientcert.pem”
AT+QSSLCFG=“clientkey”,1,“RAM:clientkey.pem”,“”
OK
AT+QSSLOPEN=1,1,1,“143.244.223.96”,443
OK
+QSSLOPEN: 1,552

AT+QSSLCFG=“cacert”,1,“UFS:cacert.pem”
AT+QSSLCFG=“clientcert”,1,“UFS:clientcert.pem”
AT+QSSLCFG=“clientkey”,1,“UFS:clientkey.pem”,“”
OK
AT+QSSLOPEN=1,1,2,“143.244.223.96”,443
OK
+QSSLOPEN: 2,552

Control test — seclevel 0 succeeds every time against same target:

AT+QSSLCFG=“seclevel”,1,0
OK
AT+QSSLOPEN=1,1,7,“143.244.223.96”,443
OK
+QSSLOPEN: 7,0

AT+QSSLOPEN=1,1,7,“www.google.com”,443
OK
+QSSLOPEN: 7,0

Summary:

  • seclevel=0 → succeeds against every target tested (own server IP, Google)

  • seclevel=1 → 552 against every target tested

  • seclevel=2 → 552 against every target tested

  • Behaviour is identical regardless of: cacert path prefix (no prefix / RAM: / UFS:), cacert content (single root 1939 bytes vs root+intermediate chain 3505 bytes, both verified byte-identical via QFREAD), clock state (1980 default and correct 2026 after NTP sync), and relaxation flags (ignorelocaltime, ignoreinvalidcertsign, ignoremulticertchainverify all set to 1)

    One additional request: if possible, could you share a minimal working example — just the AT+QSSLCFG and AT+QSSLOPEN commands — that you have personally confirmed works at seclevel=1 or seclevel=2 on EG21GGBR07A11M1G_01.002.01.002? Even connecting to a simple public HTTPS server with just a CA cert would help us confirm whether the issue is specific to our certificate files or the firmware’s seclevel validation path itself. If your example works on our module, we know the cert files are the problem. If it also fails, that confirms a firmware-level bug.

    Given all of the above, could you please share the latest firmware for EG21GGBR07A11M1G as you offered, so we can test whether this is resolved in a newer build?

    Thank you for your patience and detailed support throughout this.

    Best regards,
    Kashif

To eliminate any possibility of the issue being specific to our certificates or server, I tested seclevel=1 against www.google.com using Google’s own GTS Root R1 CA cert (1927 bytes, single cert, no client cert involved):

AT+QFLST=“gts_root.pem”
+QFLST: “gts_root.pem”,1927

AT+QSSLCFG=“cacert”,1,“gts_root.pem”
AT+QSSLCFG=“clientcert”,1,“”
AT+QSSLCFG=“clientkey”,1,“”
AT+QSSLCFG=“seclevel”,1,1
AT+QSSLCFG=“ignorelocaltime”,1,1
AT+QSSLCFG=“ignoreinvalidcertsign”,1,1
AT+QSSLCFG=“ignoremulticertchainverify”,1,1

AT+QSSLCFG=“cacert”,1
+QSSLCFG: “cacert”,1,“gts_root.pem”

AT+QSSLCFG=“seclevel”,1
+QSSLCFG: “seclevel”,1,1

AT+QSSLOPEN=1,1,0,“www.google.com”,443
OK
+QSSLOPEN: 0,552

seclevel=0 against the same target succeeds immediately.

AT+QSSLCFG=“seclevel”,1,0

OK
AT+QSSLOPEN=1,1,1,“www.google.com”,443

OK

+QSSLOPEN: 1,0

+QSSLURC: “closed”,1

This rules out everything except the firmware’s seclevel 1/2 validation path itself. Please send the latest firmware for EG21GGBR07A11M1G_01.002.01.002 so we can confirm this is resolved in a newer build.

Hi Kashif,

Sure, will send you the latest firmware through email. Please check there.

Firmware bug in 01.002.01.002 (seclevel 1/2 always returned 552) → fixed by Fazrul’s firmware update to A0.301.A0.301

Hi Kashif,

Thank you for the feedback, I guess the previous firmware is pretty old and newest firmware has some improvement made.

If you have any queries, please feel free to create new post. I will be happily to support.

1 Like