Skip to content

X.509

nXPico M carries the same X.509 functions as nXPico W: an elliptic-curve key pair (secp256r1) and a self-signed certificate, generated on the device and stored in a reserved flash region, with the private key never leaving the chip.

What differs is where the certificate goes next. nXPico W hands it to its own TLS stack; nXPico M hands it to the nRF9151’s credential manager, because on this board TLS is terminated by the modem, not by the runtime.

Identical names, signatures and return codes to nXPico W — see the full reference. The same nXPicoX509.compiled-library declares them here too:

FunctionTypeWhat it does
x509GenerateDeviceCertasynchronousgenerates the device identity
x509HasDeviceCertimmediateis an identity present?
x509GetGenerateStatusimmediategeneration status
x509GetDeviceCertLenimmediatecertificate length (typically 583–595 bytes)
x509DumpDeviceCertasynchronousprints the certificate on the serial console — not visible on this board
x509GetDeviceCertimmediatecopies the certificate into a buffer
x509GetDeviceCertChunkimmediatereads the certificate piecewise
x509GenerateCsrimmediategenerates a signing request (not validated)

Install the library as on nXPico W — see Library. Text parameters are POINTER TO BYTE, never STRING.

The nRF9151 keeps credentials in slots identified by a sec_tag, managed with AT%CMNG:

AT%CMNG=<opcode>,<sec_tag>,<type>,<content>
FieldValue used here
opcode0 = write, 1 = list, 2 = read, 3 = delete
sec_tagany number you choose — 5432 in the example
type0 root CA, 1 client certificate, 2 client private key

The command is assembled through the modem pipe: a 583-byte PEM makes a 602-byte command, ten times the 64-byte channel, so it goes in chunks with SEND = 2 and is closed with SEND = 1.

VAR
sCert : STRING(700);
nOffset : INT;
nChunk : INT := 48; (* stay well under the 64-byte channel *)
sPiece : STRING(63);
nStep : INT;
diRes : DINT;
END_VAR
CASE nStep OF
(* 1. make sure the identity exists *)
0: IF NOT x509HasDeviceCert() THEN
diRes := x509GenerateDeviceCert(ADR('CN=nxpico-m-01'), 3650, FALSE);
nStep := 1;
ELSE
nStep := 2;
END_IF
1: IF x509HasDeviceCert() THEN nStep := 2; END_IF (* ~450 ms *)
(* 2. read it into the application *)
2: diRes := x509GetDeviceCert(ADR(sCert), SIZEOF(sCert));
IF diRes = 0 THEN
nStep := 3;
END_IF
(* 3. opening of the command *)
3: GVL.sOut := 'AT%CMNG=0,5432,1,"';
GVL.wSend := 2; (* append only *)
nOffset := 0;
nStep := 4;
(* 4. the PEM, 48 bytes at a time *)
4: GVL.wSend := 0;
IF nOffset < LEN(sCert) THEN
sPiece := MID(sCert, nChunk, nOffset + 1);
GVL.sOut := sPiece;
GVL.wSend := 2;
nOffset := nOffset + nChunk;
ELSE
nStep := 5;
END_IF
(* 5. closing quote, and transmit the whole thing *)
5: GVL.sOut := '"';
GVL.wSend := 1; (* append, add CR LF, send *)
nStep := 6;
6: GVL.wSend := 0;
IF GVL.wInLen > 0 THEN
(* expect 'OK' *)
nStep := 7;
END_IF
END_CASE

The PEM’s line breaks inside the quotes are accepted by the AT interpreter — this was the last unknown of the design, and it holds.

GVL.sOut := 'AT%CMNG=1'; (* list what is stored *)
GVL.wSend := 1;

The answer names the slot, the type and the SHA-256 fingerprint of the stored credential:

%CMNG: 5432,1,"945DA7A0..."
OK

Use AT%CMNG=2,5432,1 to read the certificate itself back, and walk MODEM_INPUT_OFFSET across the response — it is far longer than the 128-byte window.

With its own certificate in the modem, the device can authenticate itself to a platform with mutual TLS, without any shared secret and without the private key ever existing outside the RP2350. The server certificate goes in the same way, as type 0 on its own sec_tag.