RENEWING TLS CERTIFICATES ON A TIBBO DEVICE BY UPLOADING THEM
Applies to: Tibbo G3 devices (e.g. TPP2W(G3))
Sample project: TLS_CER_RENEW_PUSH
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.
The CERT_UPDATE_PUSH sample keeps both on the flash disk and lets a PC replace either one by sending the new file to a TCP port on the device. It contains:
- a TLS echo server on port 8443, which uses the server bundle;
- a TLS client test, run with a short press of the MD button, which connects to a server on the PC and checks it with the CA certificate;
- two upload ports: 9100 for a new CA certificate and 9101 for a new server bundle;
- a long press of the MD button, which restores the certificates the firmware was built with.
This article shows how the certificates are stored, how to create two sets of test certificates, and how to replace each certificate and see the device use the new one.
The companion article TLS Tutorial - How to pull new certificate to devices shows the other approach, where the device downloads its new certificates over HTTPS. That approach is the one to build on for a product; see "Before using this in a product" below.
IMPORTANT: THE UPLOAD PORTS ARE NOT PROTECTED
Anyone who can reach ports 9100 and 9101 can replace the device's certificates. A new CA certificate decides which servers the device trusts, so whoever can upload one can make the device trust their own server. Use this sample on a test network only.
WHAT YOU NEED
- OpenSSL 3.x on your PC.
- Python 3 on the PC, for the test server and the upload script.
- TIDE with the CERT_UPDATE_PUSH 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.
Receiving a file
The PC connects to the upload port, sends the file and closes the connection. The device writes everything it receives to new.der, and treats the close as the end of the file. The port the file arrives on decides which certificate it replaces. Only one upload is accepted at a time; if the sender does not close the connection within 10 seconds (UPLOAD_TIMEOUT_TICKS), the upload is abandoned and the current certificate is kept.
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.
PART 2 - CONFIGURE THE CERT_UPDATE_PUSH PROJECT
Step 1. Copy server_bundle.der and ca_cert.der from set_A into the CERT_UPDATE_PUSH 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_TLS_PORT = 9443
const TLS_SERVER_PORT = "8443"
const CA_UPLOAD_PORT = "9100"
const BUNDLE_UPLOAD_PORT = "9101"
#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 PC SERVER
The device's TLS client test connects to an HTTPS server on the PC. The same server is used by the CERT_UPDATE_PULL sample to deliver files; this sample only uses it to check the CA certificate.
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_PUSH ready. TLS server on port 8443, uploads: CA certificate on 9100, server bundle on 9101. Press MD to test the CA certificate against 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_PUSH. 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_PUSH. Send data to echo."; anything you type is echoed back. Press Ctrl+C to close the connection.
Then press MD briefly to test the CA certificate. With pc_server.py using set A, the test passes:
TLS client: connecting to 192.168.1.70:9443...
TLS client: TCP connected, TLS handshake started...
TLS client: TLS established, the PC server passed the CA certificate checkpc_server.py prints "192.168.1.218: TLS handshake OK (certificate set A)".
PART 5 - THE UPLOAD SCRIPT
Save the following as push.py in the folder that contains set_A and set_B. It sends one file to the device and closes the connection:
"""Send a certificate file to the CERT_UPDATE_PUSH sample.
python push.py ca set_B/ca_cert.der
python push.py bundle set_B/server_bundle.der
python push.py bundle set_B/server_bundle.der --ip 192.168.1.218
The file is sent as-is and the connection is then closed; the device treats
the close as the end of the file. A relative path is looked up in the current
folder first, then next to this script.
"""
import argparse
import os
import socket
PORTS = {"ca": 9100, "bundle": 9101}
ap = argparse.ArgumentParser(description="Send a certificate file to CERT_UPDATE_PUSH")
ap.add_argument("kind", choices=PORTS.keys(), help="ca = CA certificate (port 9100), bundle = server bundle (port 9101)")
ap.add_argument("file", help="DER file to send")
ap.add_argument("--ip", default="192.168.1.218", help="device IP (default 192.168.1.218)")
args = ap.parse_args()
path = args.file
if not os.path.isfile(path):
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), args.file)
with open(path, "rb") as f:
data = f.read()
port = PORTS[args.kind]
with socket.create_connection((args.ip, port), timeout=5) as s:
s.sendall(data)
s.shutdown(socket.SHUT_WR)
# Wait for the device to close its side, so the whole file has been read.
try:
while s.recv(256):
pass
except OSError:
pass
print(f"Sent {len(data)} bytes of {args.file} to {args.ip}:{port}. Check the device's debug output for the result.")Usage:
python push.py ca set_B/ca_cert.der
python push.py bundle set_B/server_bundle.derAdd --ip followed by the device's address if it is not 192.168.1.218. Any tool that can open a TCP connection, send a file and close the connection works the same way.
PART 6 - RENEW THE SERVER BUNDLE
Send set B's server bundle:
python push.py bundle set_B/server_bundle.derThe device checks the file, replaces bundle.der and reboots:
Upload: server bundle from 192.168.1.70
New file OK: 2 DER object(s), 580 bytes
Upload: server bundle replaced
Rebooting to load the new certificate...
CA certificate: loaded 'ca.der', 457 bytes
server bundle: loaded 'bundle.der', 580 bytes
...Run the openssl s_client command from Part 4 again. The Peer certificate line now shows Set B. A browser or client that trusted the set A device certificate must now trust set B's instead.
PART 7 - RENEW THE CA CERTIFICATE
Send set B's CA certificate:
python push.py ca set_B/ca_cert.derThe device replaces ca.der and reboots, as above. It now trusts the set B PC server certificate only. Press MD while pc_server.py still uses set A. The test fails, because the PC presents a certificate the device no longer trusts:
TLS client: connecting to 192.168.1.70:9443...
TLS client: TCP connected, TLS handshake started...
TLS client: TLS handshake FAILED, the PC server did not pass the CA certificate checkpc_server.py shows the device's refusal:
192.168.1.218: TLS handshake failed (TLSV1_ALERT_UNKNOWN_CA)Stop it with Ctrl+C, start it with set B's certificate and press MD again. The test now passes:
python pc_server.py --cert BThis is the order to follow in a real renewal: give the device the new CA certificate first, and switch the server to its new certificate afterwards. Done the other way round, the device stops trusting the server until it receives the new CA certificate.
PART 8 - A FILE THE DEVICE REJECTS
Send a server bundle to the CA certificate port:
python push.py ca set_B/server_bundle.derThe file has two DER objects where a CA certificate has one, so the device rejects it and keeps the current certificate. It does not reboot:
Upload: CA certificate from 192.168.1.70
New file has more than 1 DER object(s)
CA certificate: REJECTED, 'ca.der' left unchangedA PEM file, an empty file or a truncated transfer is rejected the same way, with a message that says which check failed.
PART 9 - 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 certificate...
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.
BEFORE USING THIS IN A PRODUCT
The upload ports in this sample accept a file from anyone. For a product, a replacement must come from a source the device can verify:
- Download from a trusted server. The device fetches its certificates over HTTPS and checks the server with the CA certificate it already has. See the companion article [LINK: TLS_Cert_Update_PULL].
- Authenticate the upload. For example, accept uploads only over TLS with a client certificate that the device checks, or only during a service mode that requires physical access to the device.
- Keep each device's key to itself. A server bundle contains a private key. A bundle compiled into firmware gives every device that runs it the same key. For a product, give each device its own certificate and key after manufacture.
TROUBLESHOOTING
push.py reports "Connection refused" or times out
Check the device's IP address and that the device shows the ready message. Only one upload is accepted at a time, and none while the device is about to reboot.
"REJECTED" after an upload
The line before it says which check failed. The usual causes are a PEM file instead of DER, a file sent to the wrong port, or a server bundle built in the wrong order (certificate first, then key).
"Upload: ABORTED, timed out"
The sender did not close the connection within 10 seconds. Close the connection after sending the file; the close marks the end of the file.
"TLS handshake FAILED" on the MD test
The CA certificate on the device does not match 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.
"TLS client: connection closed before TLS was established"
pc_server.py is not running or port 9443 is blocked by Windows Firewall.
A browser still shows the old certificate after a server bundle renewal
Close all browser windows and reconnect; browsers reuse open connections. The openssl s_client command shows what the device presents now.
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
Article is closed for comments.