RENEWING TLS CERTIFICATES ON A TIBBO DEVICE BY DOWNLOADING THEM
Applies to: Tibbo G3 devices (e.g. TPP2W(G3))
Sample project: TLS_CER_RENEW_PULL
OVERVIEW
Certificates expire. A device that uses TLS for years will outlive the certificates it was built with, and it needs a way to receive new ones without new firmware.
A device can need two kinds of certificate:
- The server bundle (server_bundle.der), when the device is a TLS server: its own certificate and private key, which it presents to clients.
- The CA certificate (ca_cert.der), when the device is a TLS client: the certificate it uses to check the server it connects to.
In the CERT_UPDATE_PULL sample the device downloads both from an HTTPS server when you press the MD button briefly. Before it accepts anything, it checks the server with the CA certificate it already has. Only a server the device already trusts can give it new certificates, including a new CA certificate that decides which server it trusts next.
The sample also contains a TLS echo server on port 8443, which uses the server bundle, so you can see which certificate the device presents, and a long press of the MD button restores the certificates the firmware was built with. This article shows how the certificates are stored and downloaded, how to create two sets of test certificates, and how to watch the device move from the first set to the second.
The companion article TLS Tutorial - How to push new certificate to devices shows a simpler approach for test setups, where a PC uploads the certificates to the device.
WHAT YOU NEED
- OpenSSL 3.x on your PC.
- Python 3 on the PC, for the HTTPS server.
- TIDE with the CERT_UPDATE_PULL sample project.
- The IP addresses. The examples use 192.168.1.70 for the PC and 192.168.1.218 for the device; replace them with your own addresses.
All commands below are written on a single line so they work the same in Command Prompt, PowerShell and Git Bash, unless stated otherwise.
HOW THE CERTIFICATE STORE WORKS
In the earlier TLS samples the certificate is a ROM file: it is compiled into the firmware and loaded with sock.tlsinit(romfile.offset). ROM files cannot be changed while the device runs, so replacing such a certificate means building and uploading new firmware.
This sample keeps its certificates on the flash disk instead, where they can be replaced at any time:
- First boot. The flash disk is formatted if it has never been used, and the two ROM files, server_bundle.der and ca_cert.der, are copied to it. After this the ROM files are used only if the flash disk files are deleted (see "Returning to the original certificates").
- Every boot. Both flash disk files are copied into arrays in RAM. TLS is started from an array with sock.tlsinitfromarray(), not from a ROM file.
- Renewal. A new file is received into a temporary flash disk file, new.der. If it passes the checks described below, it replaces the current file and the device reboots so that the arrays are reloaded.
The flash disk files:
ca.der CA certificate, used to check the PC server
bundle.der server bundle, used by the device's TLS server
new.der a replacement while it is being received
ca.bak previous CA certificate, exists only during a replacement
bundle.bak previous server bundle, exists only during a replacementApart from where the certificate comes from, TLS works exactly as in the other samples: the same handshake calls, the same states and the same file formats. A server bundle is still the certificate followed by its private key, and ca_cert.der is still a single certificate.
Note: On a device whose flash disk has never been formatted, the sample formats it on first boot. Formatting erases anything stored on the flash disk.
Checks before a new file is accepted
The device reads the new file as a sequence of DER objects, using the length stored at the start of each one, and accepts it only if:
- every object is a DER SEQUENCE (a PEM file, which is text, fails here);
- the objects add up to exactly the file size (a truncated or padded transfer fails here);
- the number of objects is right for the target: 1 for a CA certificate, 2 for a server bundle (a file sent to the wrong target fails here);
- the file is not larger than CERT_MAX_SIZE, 3072 bytes. A transfer that goes over this limit is stopped as soon as it does.
A new file can be larger or smaller than the one it replaces. The checks do not test whether the certificate and the key in a bundle belong together, or whether a certificate is valid. Check the files on the PC before sending them.
Replacing a file safely
The current file is renamed to .bak, new.der is renamed to the current name, and the .bak file is deleted. If power fails part way through, the next boot finds the .bak file without a current file and restores it. If any step fails, the current file is kept and new.der is deleted.
A reboot does not bring back the ROM certificates: the flash disk keeps its files, and the device keeps using the latest ones it received. The files also survive uploading new firmware. CERT_UPDATE_PUSH and CERT_UPDATE_PULL use the same file names, so after running one sample, the other starts with the certificates the first one left behind.
Returning to the original certificates
Hold the MD button for 2.5 seconds (LONG_PRESS_TICKS). The device deletes its certificate files from the flash disk as soon as the time is reached, and reboots when you release the button. The boot then copies the ROM files again, exactly as on the first boot. This is refused while a transfer is in progress.
The reboot waits for the release because a Tibbo device that starts with the MD button pressed enters its monitor (firmware upload mode) instead of running the application. The same applies to the reboot after a renewal: the device waits until MD is not pressed.
The ROM files are the certificates the firmware was built with. In a real installation they may have expired by the time they are restored, which is why the samples keep this as a deliberate action.
A short press acts when the button is released, so that it can be told apart from a long press.
The size limit
Each RAM array is CERT_MAX_SIZE (3072) bytes. An ECDSA P-256 server bundle is about 580 bytes and an RSA 2048 bundle about 2 KB, so both fit. Raise CERT_MAX_SIZE in global.tbh if you need larger files; each certificate uses that much RAM.
HOW THE DOWNLOAD WORKS
A short press of MD starts two downloads, one after the other, from https://192.168.1.70:9443:
- /ca_cert.der, which replaces the CA certificate;
- /server_bundle.der, which replaces the server bundle.
For each download the device connects, checks the server with the CA certificate, sends an HTTP GET request and writes the response body to new.der. When the server closes the connection, the file is checked and committed as described above. If at least one certificate was replaced, the device reboots once both downloads have finished.
Points to note:
- Both downloads are checked with the CA certificate loaded at boot. A new CA certificate takes effect only after the reboot, so the second download is still checked against the old one.
- The request is HTTP/1.0. The server then never splits the body into chunks (chunked encoding) and marks its end by closing the connection. The device does not need to parse Content-Length; the checks on new.der catch a body that was cut short.
- The device skips the headers itself. It looks for the blank line that ends them, which may arrive split across two reads, and treats everything after it as the file. Only a 200 status is accepted.
- A failure stops the run. If the CA certificate download fails, the server bundle is not requested. The current certificates are kept, and the device reboots only if something was replaced.
- A download is abandoned after 15 seconds (PULL_TIMEOUT_TICKS).
PART 1 - CREATE TWO CERTIFICATE SETS
To see a renewal happen, you need two complete sets of certificates, A (the ones the device starts with) and B (the replacements). Each set contains:
- server_bundle.der: the device's own certificate and private key, for the device's TLS server. The certificate is issued to the device's IP address.
- ca_cert.der: the certificate of the PC server, which the device uses to check the PC. It is issued to the PC's IP address.
- pc_server_ec.crt and pc_server_ec.key: the PC server's certificate and private key, used by pc_server.py. They stay on the PC.
The two sets differ in the organisation name, "Set A" or "Set B", so you can tell them apart in OpenSSL's output, and in the validity period: set A is valid for 1 year, set B for 2 years. In a real renewal, the new certificate is the same apart from its validity dates.
The steps below use ECDSA P-256. For RSA 2048, see the TLS server article [LINK: TLS_Inbound_SSL_MULTI_SERVER] for the key commands; the bundle is built the same way.
Step 1. Create two folders, set_A and set_B, next to each other. Do Steps 2 to 6 in set_A, then repeat them in set_B with "Set B" in the configuration files and -days 730 instead of -days 365.
Step 2. Create a configuration file named device_ec.cnf for the device certificate:
[req]
prompt = no
distinguished_name = dn
x509_extensions = v3_req
[dn]
CN = 192.168.1.218
O = Tibbo cert update test - Set A
[v3_req]
basicConstraints = critical,CA:TRUE
subjectAltName = @alt_names
[alt_names]
IP.1 = 192.168.1.218
Step 3. Create the device key and certificate, and build server_bundle.der from them:
openssl ecparam -name prime256v1 -genkey -noout -out device_ec.key
openssl req -new -x509 -key device_ec.key -out device_ec.crt -days 365 -config device_ec.cnf
openssl x509 -in device_ec.crt -outform DER -out device_ec_cert.der
openssl ec -in device_ec.key -outform DER -out device_ec_key.derJoin the two DER files, the certificate first. In Command Prompt (in PowerShell, put cmd /c in front):
copy /b device_ec_cert.der + device_ec_key.der server_bundle.derIn Git Bash, Linux or macOS:
cat device_ec_cert.der device_ec_key.der > server_bundle.der
Step 4. Create a configuration file named pc_server_ec.cnf for the PC server certificate:
[req]
prompt = no
distinguished_name = dn
x509_extensions = v3_req
[dn]
CN = 192.168.1.70
O = Tibbo cert update test - Set A
[v3_req]
basicConstraints = critical,CA:TRUE
subjectAltName = @alt_names
[alt_names]
IP.1 = 192.168.1.70
Step 5. Create the PC server key and certificate, and ca_cert.der for the device. The PC server certificate is self-signed, so it is its own CA certificate:
openssl ecparam -name prime256v1 -genkey -noout -out pc_server_ec.key
openssl req -new -x509 -key pc_server_ec.key -out pc_server_ec.crt -days 365 -config pc_server_ec.cnf
openssl x509 -in pc_server_ec.crt -outform DER -out ca_cert.der
Step 6. Check the set:
openssl x509 -in device_ec.crt -noout -subject -enddate -ext subjectAltName
openssl x509 -in pc_server_ec.crt -noout -subject -enddate -ext subjectAltNameThe subjects must show the right set name and the SANs the right IP addresses. As a guide, server_bundle.der is about 580 bytes and ca_cert.der about 460 bytes. The sizes of set A and set B may differ by a byte, because the length of an ECDSA signature varies.
Self-signed certificates and the CA certificate
Because the PC server certificate is self-signed, renewing it also changes the certificate the device must trust, so ca_cert.der has to be renewed with it. When a server certificate is issued by a certificate authority, renewing it usually does not change ca_cert.der; that file only needs renewing when the authority's own certificate changes or expires.
If you have already created the sets for the CERT_UPDATE_PUSH article, use the same ones.
PART 2 - CONFIGURE THE CERT_UPDATE_PULL PROJECT
Step 1. Copy server_bundle.der and ca_cert.der from set_A into the CERT_UPDATE_PULL project folder, replacing the existing files. In TIDE, make sure both are part of the project. These are the certificates the device starts with.
Step 2. Check the settings in global.tbh:
const DEVICE_IP = "192.168.1.218" must match the IP in the device certificate
const DEVICE_MASK = "255.255.255.0"
const DEVICE_GW = "192.168.1.1"
const PC_IP = "192.168.1.70" must match the IP in the PC server certificate
const PC_HTTPS_PORT = 9443
const CA_URL_PATH = "/ca_cert.der"
const BUNDLE_URL_PATH = "/server_bundle.der"
const TLS_SERVER_PORT = "8443"
#define TLS_RX_BUFF 6 use 8 for RSA 2048
#define TLS_TX_BUFF 6 use 8 for RSA 2048
const CERT_MAX_SIZE = 3072Step 3. Build the project and upload it to the device.
PART 3 - RUN THE HTTPS SERVER
The device downloads its new certificates from an HTTPS server on the PC. The server presents the PC server certificate of one set and offers the device files of a set, which may be a different one.
Save the following as pc_server.py in the folder that contains set_A and set_B:
"""HTTPS server for the CERT_UPDATE_PUSH and CERT_UPDATE_PULL samples.
The server presents the PC server certificate of one set (--cert) and offers
the device files of a set (--serve) for download:
/ca_cert.der new CA certificate for the device
/server_bundle.der new server bundle for the device
Run it from any folder; set_A and set_B are found next to this script.
python pc_server.py certificate A, offers set B
python pc_server.py --cert B certificate B, offers set B
python pc_server.py --cert B --serve A certificate B, offers set A
"""
import argparse
import http.server
import os
import ssl
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
FILES = ("ca_cert.der", "server_bundle.der")
ap = argparse.ArgumentParser(description="HTTPS server for the CERT_UPDATE samples")
ap.add_argument("--cert", default="A", choices=["A", "B"], help="certificate set the server presents (default A)")
ap.add_argument("--serve", default="B", choices=["A", "B"], help="set whose device files are offered (default B)")
ap.add_argument("--host", default="0.0.0.0", help="address to listen on (default all)")
ap.add_argument("--port", type=int, default=9443, help="port to listen on (default 9443)")
args = ap.parse_args()
cert_dir = os.path.join(HERE, "set_" + args.cert)
serve_dir = os.path.join(HERE, "set_" + args.serve)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.minimum_version = ssl.TLSVersion.TLSv1_3
ctx.load_cert_chain(os.path.join(cert_dir, "pc_server_ec.crt"), os.path.join(cert_dir, "pc_server_ec.key"))
class Handler(http.server.BaseHTTPRequestHandler):
# HTTP/1.0: one request per connection, closed after the response.
protocol_version = "HTTP/1.0"
def do_GET(self):
name = self.path.lstrip("/")
if name not in FILES:
self.send_error(404)
return
with open(os.path.join(serve_dir, name), "rb") as f:
data = f.read()
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
print(f" sent set {args.serve} {name} ({len(data)} bytes)")
class Server(http.server.ThreadingHTTPServer):
def get_request(self):
conn, addr = self.socket.accept()
try:
tls = ctx.wrap_socket(conn, server_side=True)
print(f"{addr[0]}: TLS handshake OK (certificate set {args.cert})")
return tls, addr
except (ssl.SSLError, OSError) as e:
# The usual cause: the device does not trust this certificate set.
print(f"{addr[0]}: TLS handshake failed ({getattr(e, 'reason', None) or e})")
conn.close()
raise
def handle_error(self, request, client_address):
# A client that closes without sending a request (e.g. a handshake-only test).
print(f"{client_address[0]}: connection ended ({sys.exc_info()[1]})")
srv = Server((args.host, args.port), Handler)
print(f"HTTPS on port {args.port}, certificate set {args.cert}, offering set {args.serve}: /{FILES[0]} /{FILES[1]}")
print("Ctrl+C to stop")
try:
srv.serve_forever()
except KeyboardInterrupt:
passStart it with set A's certificate, offering set B's files (these are the defaults):
python pc_server.pyExpected output:
HTTPS on port 9443, certificate set A, offering set B: /ca_cert.der /server_bundle.der
Ctrl+C to stopAllow port 9443 through Windows Firewall when prompted. To use set B's certificate instead, stop the server with Ctrl+C and start it with --cert B. The server only offers ca_cert.der and server_bundle.der; the folder's other files, including the .key files, are not available.
PART 4 - FIRST BOOT
On the first boot the device copies the ROM files to the flash disk (formatting it first if needed), then loads them:
New file OK: 1 DER object(s), 457 bytes
CA certificate: copied from ROM file 'ca_cert.der'
New file OK: 2 DER object(s), 579 bytes
server bundle: copied from ROM file 'server_bundle.der'
CA certificate: loaded 'ca.der', 457 bytes
server bundle: loaded 'bundle.der', 579 bytes
Buffer pages free after alloc: ...
CERT_UPDATE_PULL ready. TLS server on port 8443. Press MD to download new certificates from https://192.168.1.70:9443
Hold MD for 2.5 s to restore the certificates from ROM.On later boots only the two "loaded" lines appear before the ready message. The byte counts are those of your own files. If the device has already run CERT_UPDATE_PUSH or CERT_UPDATE_PULL, you also see only the "loaded" lines, with whatever certificates that sample left on the flash disk. Hold MD for 2.5 seconds and release it to start from set A.
Check which certificate the device's TLS server presents:
openssl s_client -connect 192.168.1.218:8443 -briefExpected output (the set name appears in the depth=0 and Peer certificate lines):
Connecting to 192.168.1.218
Can't use SSL_get_servername
depth=0 CN=192.168.1.218, O=Tibbo cert update test - Set A
verify error:num=18:self-signed certificate
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN=192.168.1.218, O=Tibbo cert update test - Set A
Hash used: SHA256
Signature type: ecdsa_secp256r1_sha256
Verification error: self-signed certificate
Peer Temp Key: ECDH, prime256v1, 256 bits
Connected to CERT_UPDATE_PULL. Send data to echo.The verification error appears because OpenSSL does not trust the self-signed certificate, and "Can't use SSL_get_servername" because the connection is made by IP address, not by name. Neither matters here. The device sends "Connected to CERT_UPDATE_PULL. Send data to echo."; anything you type is echoed back. Press Ctrl+C to close the connection.
PART 5 - DOWNLOAD THE NEW CERTIFICATES
With pc_server.py using set A's certificate and offering set B (the defaults), press MD. The device trusts set A, so it accepts the server and downloads both set B files:
Download: CA certificate, connecting to 192.168.1.70:9443...
Download: TLS established, GET /ca_cert.der
New file OK: 1 DER object(s), 457 bytes
Download: CA certificate replaced
Download: server bundle, connecting to 192.168.1.70:9443...
Download: TLS established, GET /server_bundle.der
New file OK: 2 DER object(s), 580 bytes
Download: server bundle replaced
Rebooting to load the new certificates...
CA certificate: loaded 'ca.der', 457 bytes
server bundle: loaded 'bundle.der', 580 bytes
...pc_server.py shows each request:
192.168.1.218: TLS handshake OK (certificate set A)
192.168.1.218 - - [...] "GET /ca_cert.der HTTP/1.0" 200 -
sent set B ca_cert.der (457 bytes)
192.168.1.218: TLS handshake OK (certificate set A)
192.168.1.218 - - [...] "GET /server_bundle.der HTTP/1.0" 200 -
sent set B server_bundle.der (580 bytes)Run the openssl s_client command from Part 4 again. The Peer certificate line now shows Set B.
PART 6 - THE DEVICE NOW TRUSTS ONLY THE NEW SERVER CERTIFICATE
Press MD again while pc_server.py still uses set A. The device now has set B's CA certificate, so it refuses the server:
Download: CA certificate, connecting to 192.168.1.70:9443...
Download: CA certificate FAILED, TLS handshake failed, the server did not pass the CA certificate check
Download: no certificate was replacedpc_server.py shows the device's refusal:
192.168.1.218: TLS handshake failed (TLSV1_ALERT_UNKNOWN_CA)Stop pc_server.py with Ctrl+C, start it with set B's certificate and press MD again. The download now succeeds. The server offers set B, which the device already has, so the files are replaced by identical ones and the device reboots:
python pc_server.py --cert BTo move the device back to set A by download, start the server with --cert B --serve A, press MD, then restart the server with --cert A. The quicker way is the long press described in Part 7.
The order of a renewal
This exercise shows the order a real renewal must follow:
- While the server still uses its current certificate, the device downloads the new CA certificate (and, if needed, its own new server bundle).
- Only after the devices have the new CA certificate does the server switch to its new certificate.
A device that misses step 1 no longer trusts the server after step 2, and cannot download anything from it. Such a device can only be recovered another way: new firmware, or a method like the one in the CERT_UPDATE_PUSH article. Plan renewals well before the current certificates expire, and keep the old server certificate in service until every device has the new CA certificate.
PART 7 - RETURN TO THE ORIGINAL CERTIFICATES
Hold MD for 2.5 seconds, until the first line below appears, then release it. The device reboots and copies set A from ROM again:
MD long press: certificates deleted from the flash disk. Release MD to reboot.
Rebooting to load the new certificates...
New file OK: 1 DER object(s), 457 bytes
CA certificate: copied from ROM file 'ca_cert.der'
New file OK: 2 DER object(s), 579 bytes
server bundle: copied from ROM file 'server_bundle.der'
CA certificate: loaded 'ca.der', 457 bytes
server bundle: loaded 'bundle.der', 579 bytes
...The device is back where it started: the openssl s_client command shows Set A, and the device trusts the set A PC server certificate. Start pc_server.py with --cert A (the default) to match.
Debug output
Set PULL_DEBUG to 1 in global.tbh to print every read of the response, the position where the headers end and the state when the connection closes. This helps when trying a different web server.
BEFORE USING THIS IN A PRODUCT
Checking the server protects the device: it accepts certificates only from a server it trusts. It does not protect the server bundle, which contains the device's private key. In this sample pc_server.py hands the bundle to any client that asks for it. For a product:
- The server must identify the device before it hands out a bundle, for example with mutual TLS, where the device presents its current certificate. See the mTLS article [LINK: TLS_mTLS_HTTPS_MTLS].
- Each device gets its own bundle, with its own key, at a path the server chooses for that device.
- Use HTTPS only. The CA certificate decides which servers the device trusts. A download over plain HTTP could be replaced on the way.
TROUBLESHOOTING
"TLS handshake failed, the server did not pass the CA certificate check"
The device does not trust the certificate pc_server.py presents. Start pc_server.py with the set whose ca_cert.der the device has, and check that PC_IP matches the IP in that certificate.
"connection closed before the file was received"
pc_server.py is not running, port 9443 is blocked by Windows Firewall, or the server closed the connection early.
"connection closed before the file was received" although the server log shows the file was sent
Set PULL_DEBUG to 1 and check whether "end of headers" appears. If it does not, the headers did not end with a blank line (CR LF CR LF).
"server answered 'HTTP/1.0 404'"
The server does not have the requested file. Check CA_URL_PATH and BUNDLE_URL_PATH, and that the set folder pc_server.py offers contains ca_cert.der and server_bundle.der.
"REJECTED" after a download
The line before it says which check failed. With another web server, check that it sends the file unchanged and does not use chunked encoding for HTTP/1.0 requests.
"timed out"
The download did not finish within 15 seconds. Check the network, and that the server closes the connection after sending the file.
The device stops after "Rebooting..." and Device Explorer shows it in monitor (firmware upload) mode
The device restarted while the MD button was pressed. Power-cycle it without pressing MD. The application reboots only after MD is released, so this happens only if MD is pressed again during the reboot.
"fd.formatj FAILED" or "fd.mount FAILED" at boot
The flash disk could not be prepared, so no certificate can be stored. The device then refuses TLS connections. Check that the device has flash disk space available.
Comments
0 comments
Please sign in to leave a comment.