SSL Application using Qeuctel Modem

SSL Application using Qeuctel Modem

"A dive into SSL application with Quectel EC200U modem"

Gideon Maina

Gideon Maina

Senior IoT Engineer

April 15, 2026
6 min read
GenAI SSL with Quectel

I stumbled upon a problem when I needed to connect Supabase via Quectel modem paired with an ESP32S3 module. I was using a certificate that I had generated for another earlier project to use on as an underpowered chip. Everything with the code seemed okay but connection to Supabase was refused.

As it may be obvious, either the procedure SSL procedure was wrong or the certificate was invalid. The next step was to get the CA bundle tha Supabase uses and it worked! So I wll break down the steps that you need to make things work.

Understanding Certificate Requirements

Based on the EC200U SSL application not, 3 different authentication levels are supported via the seclevel parameter:

seclevel=0: No authentication (not recommended)
seclevel=1: Server authentication only (requires CA certificate)
seclevel=2: Mutual authentication (requires CA cert + client cert + client key)

Since Supabsase requires authentication, I set the seclevel =1. In fact, for most appplications, you do NOT need client certificates unless the server specifically requires mutual TLS authentication.

Get the supabase certificate

bash
openssl s_client -showcerts -connect :443 /dev/null | \
openssl x509 -outform PEM > server_ca.pem

Replace remoteserver.com with your target address.

Upload your certificate to the Quectel modem

Copy the contents of the server_ca.pem file and save them in a variable. For Arduino framework PROGMEM variable, it would look something like this.

const char SUPABASE_CHAIN_PEM[] PROGMEM =
    "-----BEGIN CERTIFICATE-----\n"
    "MIIDpjCCA0ygAwIBAgIRAKK/z/J073aiE5pdWZEz2j0wCgYIKoZIzj0EAwIwOzEL\n"
    .
	.
	.
    "A0gAMEUCIQCyZT67c4SDLY7zBVSGXwB5v/F1s+BD8Sic9WSUPA6jbgIgNroP7rZj\n"
    "rr5/RNJpbO2mVKXcn47jGWnOPDDB9o3xo7c=\n"
    "-----END CERTIFICATE-----\n"
    "-----BEGIN CERTIFICATE-----\n"
    "MIICnzCCAiWgAwIBAgIQf/MZd5csIkp2FV0TttaF4zAKBggqhkjOPQQDAzBHMQsw\n"
    "CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU\n"
	.
    .
    "AOcCq1HW90OVznX+0RGU1cxAQXomvtgM8zItPZCuFQ8jSBJSjz5keROv9aYsAm5V\n"
    "sQIwJonMaAFi54mrfhfoFNZEfuNMSQ6/bIBiNLiyoX46FohQvKeIoJ99cx7sUkFN\n"
    "7uJW\n"
    "-----END CERTIFICATE-----\n"
    "-----BEGIN CERTIFICATE-----\n"
    "MIIDejCCAmKgAwIBAgIQf+UwvzMTQ77dghYQST2KGzANBgkqhkiG9w0BAQsFADBX\n"
    .
    .
    "8RqZ7a2CPsgRbuvTPBwcOMBBmuFeU88+FSBX6+7iP0il8b4Z0QFqIwwMHfs/L6K1\n"
    "vepuoxtGzi4CZ68zJpiq1UvSqTbFJjtbD4seiMHl\n"
    "-----END CERTIFICATE-----\n";

As you can see it is a really long string with redacted content that why I chose to store it in the program memory. Note the way each line is wrapped with quote marks. Alternatively, you can use R"(Your long certificate string)" to avoid the extra work of formating it with quote, though that can be done easily by a tool.

Choose a the name you are going to save the certificate file in the modem. E.g. const char SUPABASE_PEM_FILENAME[] PROGMEM = "supabasechain.pem";

Upload the CA contents.

  1. AT+QFOPEN="<filename": wait till you get an OK response and extract the file handle number; very important! You need to save it somewhere or the modem file system will not behave as expected. I personally do not like the extra baggage of managing file numbers instead of file names but hey, what do I know about writing AT applications.
    The output looks like this:

	+QFOPEN: 
	
	
	OK

2.AT+QFWRITE=<file_handle_number>, <file_size> : The file size can be gotten via sizeof() or strlen().
3. Wait until you get the CONNECT keyword in the response,which sets the modem to transparent mode, and write the CA content. The modem will return to command mode when written bytes are equal to file size.
4. Confirm if the file was saved because you have trust issues like me.

  • AT+QFSEEK=<file_handle_number>,0,0

  • AT+QFREAD=<file_handle_number>, <file_size>: wait for the CONNECT keyword and read the stream. Print it and compare with the CA content you extracted earlier and rest easy.

  1. Close the file: AT+QFCLOSE=<file_handle_number>

Establish an SSL connection

I started with defining various parameters according to the SSL application note as I cannot keep up with numbers.

c/c++

// SSL Versions
#define SSL_VERSION_SSL30 0
#define SSL_VERSION_TLS10 1
#define SSL_VERSION_TLS11 2
#define SSL_VERSION_TLS12 3
#define SSL_VERSION_ALL 4

// Authentication modes
#define SSL_SECLEVEL_NONE 0              // No authentication
#define SSL_SECLEVEL_SERVER 1            // Server authentication only
#define SSL_SECLEVEL_SERVER_AND_CLIENT 2 // Server and client authentication

// Cipher suites
#define SSL_CIPHER_ALL "0xFFFF"
#define SSL_CIPHER_AES_256_CBC_SHA "0x0035"
#define SSL_CIPHER_AES_128_CBC_SHA "0x002F"
#define SSL_CIPHER_RC4_128_SHA "0x0005"
#define SSL_CIPHER_RC4_128_MD5 "0x0004"
#define TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 "0XC027"

1. Configure SSL

Quectel manages SSL sessions with context IDs. These are different from PDP context IDs ot TCP/IP client IDs as per the TCP/IP application note. Keep in my SSL connections use TCP/IP and each of the 8 SSL context IDs can suppport upto 12 secure TCP/IP clients. I know, that's just how Quectel manages sessions.

I will use context id 3 as const uint8_t DEFAULT_SSL_CTX_ID = 3; for this case.

  • Set SSL version: AT+QSSLCFG="sslversion",<DEFAULT_SSL_CTX_ID>,<SSL_VERSION_TLS12>

  • Set CA path : AT+QSSLCFG="cacert",<DEFAULT_SSL_CTX_ID>,"<cert_path>" : Replace certificate path with the name you set for the filename. In my case that was UFS:supabasechain.pem . You don't have to prefix it with UFS: if you stored the file in the modem's flash memory unless you are using external storage with the Quectel modem.

  • Set SNI (Server Name Indication) : AT+QSSLCFG="sni",<DEFAULT_SSL_CTX_ID>,1: This is because we are using TLS protocol here.

  • Set security level: `AT+QSSLCFG="seclevel",,

  • Ignore local time: AT+QSSLCFG="ignorelocaltime",<DEFAULT_SSL_CTX_ID>,1

  • Set Cipher Suite: AT+QSSLCFG="ciphersuite",<DEFAULT_SSL_CTX_ID>,<SL_CIPHER_ALL>

Open an SSL connection

The AT command to open a secure connection is as follows:
AT+QSSLOPEN=<contextID>,<SSL_ctxID>,<clientID>,"<serveraddr>",<server_port>[,<access_mode>]

  • contextID is the PDP context that you set during the GPRS initialization.

  • SSL_ctxID is the context id (0-5) you chose.We used DEFAULT_SSL_CTX_ID = 3

  • clientID is a unique client id for every SSL connection. Quectel EC200 can handle 12 clients or socket identifier per context ID i.e 0-11

  • serveraddr can be an IP or domain. The port is self expanatory at this point.

  • access_mode (0-3) is the access mode of SSL connection. I used buffer access mode for the modem to buffer the received data: this is the default access mode

0 Buffer access mode
1 Direct push mode
2 Transparent access mode

In buffer access or direct push mode, you will expect a response in the format +QSSLOPEN: <clientID>,<err> with err=0being a successful connection.

Exchange data between the connection.

At this point this is really up to you now. However, I will show you how to post data and receive a response from the server via HTTP(S). The S denotes that the data is being sent over a secure connection which we have already estabilishe but stripped down it is just bare HTTP.

Prepare your HTTP data

Before openning the secure TCP connection, you would probbably want to build your HTTP payload.
Here is a template example of a Supabase payload I used to send GPS data to a table.

POST /rest/v1/ HTTP/1.1
Host: .supabase.co
Authorization: Bearer 
apikey: 
Content-Type: application/json
User-Agent: Quectel-Giddie
Prefer: return=minimal
Content-Length: 

{"latitude":-1.30456000,"longitude":32.27153000,"_time":"2025-12-18T14:51:45.000Z"}

Send over SSL

  • AT+QSSLSEND=<clientID>,<sendlen> : client id you used to open an SSL connection with. Send length is the size the size of the entire HTTP payload.

  • Wait for > character in the response.

  • Write the data. On Arduino, use a serial instance which inherits from Stream class like so _stream->print(<http_payload>);

  • Wait for QURC +QSSLURC: "recv",<clientID> : If the client id matches the client id for your connection, you have received a response from the server.

Read the response

  • Read specified bytes of the repsonse: AT+QSSLRECV=<clientID>,<readlen> : If you want to read all the data from the response, keep calling this with manageable read lengths until buffer is empty +QSSLRECV: 0

  • Close the connection: AT+QSSLCLOSE=<clientID>[,<close_ timeout_seconds>].

Summary

Now you are set to explore the full potential of the Quectel modem. Just keep in mind of these key things.

  • You need a CA bundle to establish secure connections.

  • SSL application works on top of TCP/IP layer

  • A GPRS connection must be established beforehand

  • Quectel SSL application used a valid GPRS PDP context id.

  • SSL context id is used by the Quectel modem to manage SSL sessions. Not to be confused by the PDP context.

  • An SSL client id used internally by the Quectel modem to manage sessions for a particular SSL context id.

Gideon Maina

About Gideon Maina

Senior IoT Engineer

IoT Engineer and Full-stack Developer passionate about building innovative solutions for African challenges. Experienced in sensor networks, web development, and data visualization.