A proof-of-concept certificate authority (CA) and bootstrap server written in Go.
This utility provides an authenticated REST API that can be used to:
- Validate and sign certificate signing requests (CSRs)
- Register devices on a 3rd party cloud service during certificate registration
- Provision devices with basic connection details (MQTT broker domain/port)
- List previously registered devices
- Check certificate validity
Certificate requests are logged to a local SQLite database (CADB.db
).
Device registration hooks to cloud services and returned MQTT broker details are currently based on Azure IoT Hub, which may be extended to other providers in the future.
It also enables a basic TCP server (mTLS TCP server
) that requests a client
certificate during the TLS handshake, verifying that the certificate has been
signed by the CA. This is only used for testing and proof of concept purposes.
The REST API will only accept requests from client devices with access to the bootstrap certificate and private key generated by
setup-bootstrap.sh
further below in this guide. It's assumed that this certificate and key pair will be provisioned to devices in the factory. This only provides a nominal level of protection, but demonstrates a starting point for securing the server from unwanted access.
This project requires a recent version of go to compile and debug the server.
For platform-specific details on installing go, see: https://go.dev/doc/install
If you haven't already, the first step is to clone and build the app locally:
$ git clone https://github.com/Linaro/lite_bootstrap_server.git
$ cd lite_bootstrap_server
$ go build -o liteboot
The
-o liteboot
is optional, but will cause the compiled app to have the shorterliteboot
name instead of the cumbersome default oflite_bootstrap_server
.
You can override various config settings with a .liteboot.toml
file in the
application root folder.
The [server]
settings, which map to the command-line parameters shown with
liteboot server --help
, must be populated to set the ports used by the
utility, as well as to provide a response to the ccs
endpoint:
[server]
# Azure IoT Hub Settings
hubname = "azure-hub-name"
resourcegroup = "azure-resource-group"
mqttport = 8883
# Set the server hostname explicitly
# hostname = "myhostname.local"
# CA port number (REST API)
port = 1443
# mTLS port number
mport = 8443
The hostname is important since it will be included in the subject line of certificates signed by liteboot, and be part of the SERVER certificate used to establish a TLS connection to the server.
The same hostname must be used in the SERVER certificate or the TLS connection will be refused.
This application will attempt to determine the hostname to use for the server(s) based on the following order of precedence:
- Via the
--hostname
parameter in theserver
command - Via a
hostname
entry in the.liteboot.toml
config file (see step 1 above) - Check for the
$CAHOSTNAME
environment variable - Check the system hostname (same value as
hostname
in shell) - Default to
localhost
as a last resort
For example, set the hostname before starting the server via:
$ export CAHOSTNAME='myhostname.local'
Which could also be overridden via:
$ ./liteboot server start --hostname="localhost"
For local development, this can usually be set to localhost
, but will need
to be changed when working with multiple machines or containers.
First, create the HTTP and CA keys and certificates via setup-ca.sh
.
This should only need to be run once, and, in fact, will require existing certs to have to be removed manually before it can be run again:
If you haven't set the hostname via the
CAHOSTNAME
environment variable, and wish to use a hostname other than what thehostname
command resolves to, this script should be called with the hostname as the first parameter. Run the script with-h
for details.
$ ./setup-ca.sh
Next, setup the bootstrap key pair via setup-bootstrap.sh
.
This key pair is required to authenticate with the CA server, and the data
placed in bootstrap_crt.txt
and bootstrap_key.txt
will need to be
included in the client device, or included in any attempts to connect to the
REST API using curl
, wget
, etc.
The script can be run multiple times (after removing the generated files), but only would need to be rerun after regenerating the CA cert.
$ ./setup-bootstrap.sh
Run run-server.sh
to start the CA server on port 1443, or whatever port you
set in the config file:
This script also accepts an optional hostname override as the first parameter. Run the script with
-h
for details.
$ ./run-server.sh
You can optionally run new-device.sh
to create a new device, and registers
it with this CA.
The certificates for the device will be placed in the 'certs' directory, along with the certificates above.
This certificate is only useful for testing the CA, and generally will not be used in a production setting.
This script also accepts an optional hostname override as the first parameter. Run the script with
-h
for details.
$ ./new-device.sh
You can remove all existing certificate artifacts via:
$ rm CADB.rb
$ rm -rf certs
The REST API is a work in progress, and may be changed in the future!
Request a certificate for a new device, based on the provided certificate signing request (CSR).
The CA will assign and record a unique serial number for this certificate,
which can later be used to check the certificate status via the cs/{serial}
endpoint.
This API requires the
Content-Type
to be set on the post data, and must be set to eitherapplication/cbor
orapplication/json
. A request made in cbor will have a cbor reply, json will reply with a json encoded response.
The CSR payload should be wrapped in a single CBOR array:
[ bstr ]
See the new-device.sh
script for an example of generating an appropriate
request from the shell, and saving the response.
Replies with a CBOR array containing the following fields:
{
1 => int, ; Status.
2 => bstr, ; Certificate
}
Status
is an error code where0
indicates success, and non-zero values should be treated as an error.Certificate
contains the BASE64-encoded DER format certificate.
Takes a BASE64-encoded CSR payload in a JSON wrapper as follows:
{
"CSR":"MIH9MIGlAgEAMEMxEjAQBgNVBAoMCWxvY2FsaG9zdDEtMCsGA1UEAwwkZThjNDdiNDItZjdmYy00ZGM4LWI1MzgtOTM0OTZiNjE5YTNjMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJ5aLGLJID8MVHCHEzQOlO63RvBaTy8lRbtkDODYPgDKBOuAHWXbytgjO32K8O282BK/Hl5eEKqcXcHerlxE2xKAAMAoGCCqGSM49BAMCA0cAMEQCIEFoH+NV9jXJA0PmctbCQ7FOBnE/aT9hmqBuvBq2kIhuAiAyKGAUIAzHDBZ+lY6WaJGh/56rzr4KprVtNYFGLHLs1g=="
}
⚠️ TheCSR
field in the JSON request payload is a BASE64 encoded byte array from a DER file. This paylaod doesn't include the text headers present in the PEM files generated byopenssl req -new -key UUID.key -out UUID.csr
innew-device.sh
.
Replies with a JSON array containing Status
, and Cert
fields:
Status
is an error code where0
indicates success, and non-zero values should be treated as an error.Cert
contains the BASE64-encoded DER format certificate.
{
"Status":0,
"Cert":"MIIBtjCCAVugAwIBAgIIFrKA6WV+D5gwCgYIKoZIzj0EAwIwOjEUMBIGA1UEChMLTGluYXJvLCBMVEQxIjAgBgNVBAMTGUxpbmFyb0NBIFJvb3QgQ2VydCAtIDIwMjAwHhcNMjExMDI5MTI0MjM0WhcNMjIxMDI5MTI0MjM0WjBDMRIwEAYDVQQKEwlsb2NhbGhvc3QxLTArBgNVBAMTJGU4YzQ3YjQyLWY3ZmMtNGRjOC1iNTM4LTkzNDk2YjYxOWEzYzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCeWixiySA/DFRwhxM0DpTut0bwWk8vJUW7ZAzg2D4AygTrgB1l28rYIzt9ivDtvNgSvx5eXhCqnF3B3q5cRNsSjQjBAMB0GA1UdDgQWBBSCv0wCNUXMXavfi15AbcCclDvfkzAfBgNVHSMEGDAWgBSxhUrvHyyKgHn5/FaoKd761df1tjAKBggqhkjOPQQDAgNJADBGAiEAjDVYvr1qBfvc0VFZcFLxwO/5XvnBh2jZFpL9ykKsCw8CIQDF3ne7yokRAHt0nn35CW/J3FclGYH9rBVCZr7FU+pzHg==",
}
This endpoint is used to request a certificate for a new device, posting a PKCS#10 CSR file in PEM format.
The CA will assign and record a unique serial number for this certificate,
which can later be used to check the certificate status via the cs/{serial}
endpoint.
It will reply with a certificate file in PEM format or a status error in JSON, depending on the input CSR provided.
Testing this endpoint with:
The example belows assumes
MBP2021.lan
as the local hostname. This value will vary from one machine to another.
$ curl -v --cacert certs/CA.crt \
--cert certs/BOOTSTRAP.crt \
--key certs/BOOTSTRAP.key \
-F [email protected] \
--output USER.crt \
https://MBP2021.lan:1443/api/v1/p10cr
should give you a USER.crt
file in PEM format, which you can view via:
$ openssl x509 -in USER.crt -noout -text
Checks if any valid certificates are associated with the specified device UUID. If any valid certificates are found, their serial number will be returned in the response payload.
This endpoint accepts requests in cbor (application/cbor
) or json
(application/json
), defaulting to JSON responses if no Content-Type is
provided. The response Content-Type will match the request type used.
$ curl -v --cacert certs/CA.crt \
--cert certs/BOOTSTRAP.crt \
--key certs/BOOTSTRAP.key \
https://MBP2021.lan:1443/api/v1/ds/56d38f73-3f6f-4a59-86bc-d315a1ccc634
You can send a status request via:
$ curl -v --cacert certs/CA.crt \
--cert certs/BOOTSTRAP.crt \
--key certs/BOOTSTRAP.key \
-H 'Content-Type: application/cbor' \
https://MBP2021.lan:1443/api/v1/ds/56d38f73-3f6f-4a59-86bc-d315a1ccc634 \
-o cbor_enc.raw
CBOR requests receive binary responses, which curl will print as an unknown symbol on the console. Pass the
-o <filename>
argument to store the binary output in a file, and executego run cbor_decoder.go -i cbor_enc.raw -r ds
to decode the binary output response and print in the JSON format.
This endpoint will return the following responses, in JSON or CBOR encoding
depening on the Content-Type
used:
- HTTP response code 200
{"Status":0,"Serials":null}
: No valid certs found for UUID{"Status":1,"Serials":[1649073924922206000]}
: Valid cert(s) found for UUID
- HTTP response code 400 +
{"error": "<error msg>"}
, where error msg is:need a valid UUID
: Invalid or improperly formatted UUID was providedunable to query db for UUID
: error querying for database UUID (fatal)bad request: Content-Type must be ...
: Invalid Content-Type provided
Requests the certificate status based on the supplied certificate serial number.
The serial number is generated by the CA during the certificate generation
process (/ap1/v1/cr
), and is a unique timestamp-based 64-bit integer (ex.
1635511354607407000
) that is added to the certificate before sending it back
to the requesting device.
It can be retrieved using the /api/v1/ds/{uuid}
endpoint, or from a
certificate file directly via the serial number field, for example:
The example belows assumes a specific device UUID, and
MBP2021.lan
as the local hostname. These values will vary from one machine to another.
$ openssl x509 -in certs/a8c6f808-b659-4f88-affb-40498834c572.crt -noout -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 1648935985023194000 (0x16e2328abcaa9390)
Signature Algorithm: ecdsa-with-SHA256
Issuer: O=Linaro, LTD, CN=LRC - 20220330062226
Validity
Not Before: Apr 2 21:46:25 2022 GMT
Not After : Apr 2 21:46:25 2023 GMT
Subject: O=MBP2021.lan, OU=LinaroCA Device Cert - Signing, CN=a8c6f808-b659-4f88-affb-40498834c572
...
Once you have the serial number, you can send a status request via:
You can send a status request via:
$ curl -v --cacert certs/CA.crt \
--cert certs/BOOTSTRAP.crt \
--key certs/BOOTSTRAP.key \
https://MBP2021.lan:1443/api/v1/cs/1648935985023194000
You can send a status request via:
$ curl -v --cacert certs/CA.crt \
--cert certs/BOOTSTRAP.crt \
--key certs/BOOTSTRAP.key \
-H 'Content-Type: application/cbor' \
https://MBP2021.lan:1443/api/v1/cs/1648935985023194000 \
-o cbor_enc.raw
CBOR requests receive binary responses, which curl will print as an unknown symbol on the console. Pass the
-o <filename>
argument to store the binary output in a file, and executego run cbor_decoder.go -i cbor_enc.raw -r cs
to decode the binary output response and print in the JSON format.
This endpoint will return the following responses, in JSON or CBOR depending
on the Content-Type
used:
- HTTP response code 200
{"status": "1"}
: Indicates that the serial number exists, and that the certificate is marked as valid in the CA database.{"status": "0"}
: Indicates that the serial number exists, but that the certificate is marked as invalid in the CA database (i.e., it has been revoked).
- HTTP response code 400 +
{"error": "<error msg>"}
, where error msg is:invalid request
: Poorly formatted serial number was providedinvalid serial number
: No certificate matching supplied serial foundbad request: Content-Type must be ...
: Invalid Content-Type provided
Requests a copy of the x509 certificate associated with the supplied serial number. If found, the certificate data is returned in PEM format (BASE64 ASCII).
See the
cs
endpoint for details on retrieving a certificate serial number.
You can send the x509 certificate copy request via:
$ curl -v --cacert certs/CA.crt \
--cert certs/BOOTSTRAP.crt \
--key certs/BOOTSTRAP.key \
https://MBP2021.lan:1443/api/v1/cc/1664276589086394391
You can send the x509 certificate copy request via:
$ curl -v --cacert certs/CA.crt \
--cert certs/BOOTSTRAP.crt \
--key certs/BOOTSTRAP.key \
-H 'Content-Type: application/cbor' \
https://MBP2021.lan:1443/api/v1/cc/1664276589086394391 \
-o cbor_enc.raw
CBOR requests receive binary responses, which curl will print as an unknown symbol on the console. Pass the
-o <filename>
argument to store the binary output in a file, and executego run cbor_decoder.go -i cbor_enc.raw -r cc
to decode the binary output response and print in the JSON format.
Replies with a JSON or CBOR array containing Status
, and Cert
fields:
Status
is an error code where0
indicates success, and non-zero values should be treated as an error.Cert
contains the string type x509 pem format certificate.
For example, if the Content-Type
is set to the default application/json
:
{
"Status":0,
"Cert":"-----BEGIN CERTIFICATE-----\nMIIB6TCCAY+gAwIBAgIIFxiyvXR+SBcwCgYIKoZIzj0EAwIwNTEUMBIGA1UEChML\nTGluYXJvLCBMVEQxHTAbBgNVBAMTFExSQyAtIDIwMjIwOTI3MTEwMjE3MB4XDTIy\nMDkyNzExMDMwOVoXDTIzMDkyNzExMDMwOVowfDEiMCAGA1UEChMZYXJtLXZpcnR1\nYWwtbWFjaGluZS5sb2NhbDEnMCUGA1UECxMeTGluYXJvQ0EgRGV2aWNlIENlcnQg\nLSBTaWduaW5nMS0wKwYDVQQDEyRjNTJmN2Y4NS04ZmYwLTQzOGQtOGEwNi05Mzlj\nOWJlNTlhNGYwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARrMDR1HgkP/ET4yNqG\nM5km89r2DFAAKZDyL2lVg3C8sjNVrxoR73G7nhC8EyCFMwrIMK99O92tGFWOWqaM\nb2r9o0IwQDAdBgNVHQ4EFgQUDBH9jc9zvi3un83TFPI/tHhMF8YwHwYDVR0jBBgw\nFoAU+m+L2p3gV7JPexhVX6PFWMFy9cowCgYIKoZIzj0EAwIDSAAwRQIgM5zSdrUY\n7f0QG3cq+5MVL/rAb4k4Ok6vQIE63zRwQt8CIQCsQ7NQV9Sk8y3YU0R3HGUlOOMI\nKKmII4ZjIWrZs7Qr/A==\n-----END CERTIFICATE-----\n"
}
Requests the cloud connectivity details for the MQTT broker associated with your cloud infrastructure.
This endpoint allows device to retrieve details about the MQTT broker, etc., that they should connect to, and is usually retrieved during the provisioning process, or when an alert is received by the client device that the cloud connectivity settings have changed.
You can send a status request via:
$ curl -v --cacert certs/CA.crt \
--cert certs/BOOTSTRAP.crt \
--key certs/BOOTSTRAP.key \
-H 'Content-Type: application/cbor' \
-H 'Accept: application/cbor' \
https://MBP2021.lan:1443/api/v1/ccs \
-o cbor_enc.raw
CBOR requests receive binary responses, which curl will print as an unknown symbol on the console. Pass the
-o <filename>
argument to store the binary output in a file, and executego run cbor_decoder.go -i cbor_enc.raw -r ccs
to decode the binary output response and print in the JSON format.
You can send a status request via:
$ curl -v --cacert certs/CA.crt \
--cert certs/BOOTSTRAP.crt \
--key certs/BOOTSTRAP.key \
https://MBP2021.lan:1443/api/v1/ccs
Replies with a JSON array containing the following fields:
{
"Hubname":"azure_hubname",
"Port":8883
}
Hubname
contains the Azure IoT Hub hubname string.Port
contains the Azure IoT Hub MQTT port number.
Request an update to an existing (non-revoked and non-expired) certificate. An update is a replacement certificate containing either a new subject public key or the current subject public key.
Requests the revocation of an existing certificate registration.
A secondary TCP server is started up along with the main CA server to test mutual TLS authentication using client certificates.
mutual TLS authentication requests a certificate from the connecting client device that has been signed with the CA, adding an additional level of trust on behalf of the server concerning the client device.
Once a user certificate has been generated (via new-device.sh
), you can test
mTLS connections via:
The example belows assumes a specific device UUID, and
MBP2021.lan
as the local hostname. These values will vary from one machine to another.
$ openssl s_client \
-cert certs/f269528d-ff66-4fb0-83d8-e449e0038010.crt \
-key certs/f269528d-ff66-4fb0-83d8-e449e0038010.key \
-CAfile certs/CA.crt \
-connect MBP2021.lan:8443
- The
cert
andkey
fields indicate the client certificate and key - The
CAfile
field is the CA certificate
If the connection is successful, you should get the following response at the end of the outptut:
Verify return code: 0 (ok)
And liteboot will display details about the client cert in the log output:
$ ./liteboot server start
Starting mTLS TCP server on MBP2021.lan:8443
Starting CA server on port https://MBP2021.lan:1443
Connection accepted from 127.0.0.1:60510
[Certificate 0]
- Subject: CN=f269528d-ff66-4fb0-83d8-e449e0038010,OU=Signing,O=Linaro\, LTD
- Serial: 1671014018808547000
[Certificate 1]
- Subject: CN=LRC - 20221129052020,O=Linaro\, LTD
- Serial: 2022001129172020
Certificate 0
is the device certificateCertificate 1
is the CA certificate used to sign certificate 0.
To test with an invalid user certificate, generate a new cert:
$ openssl ecparam -name prime256v1 -genkey -out USERBAD.key
$ openssl req -new -x509 -sha256 -days 365 \
-key USERBAD.key -out USERBAD.crt \
-subj "/O=Linaro, LTD/CN=MBP2021.lan"
Then try the request again:
$ openssl s_client \
-cert USERBAD.crt -key USERBAD.key \
-CAfile certs/CA.crt \
-connect MBP2021.lan:8443
You should get the following error from liteboot, since the device certificate has not been signed by our CA:
Connection accepted from 127.0.0.1:50443
Client handshake error: tls: failed to verify client certificate: x509: certificate signed by unknown authority
Run openssl rand -writerand .rnd
to generate the missing .rnd file.