Using OpenSSL and Azure Functions as a Certificate Authority for Azure IoT Devices (and IoT Edge)

Often, I have partners ask for help on the complexity of setting up a PKI or Certificate Infrastructure for their IoT Devices. I have blogged in the past about approaches: Using a real certificate with the Azure IoT Client C SDK and the Provisioning Service, Using Windows Certificate Service to issue Azure IoT Edge Certificates and Using Windows Certificate Services with Azure IoT Device Provisioning Service.

Today’s blog will show how, in just a few lines of code and an Azure Function running on Linux, we can have an on demand Certificate Authority for testing or as a foundation for a production solution.

The end goal is a simple UI as shown below — or you can just make the API calls direct from your application to request new certificates or get the issuing authority public certificate.

Once you type in a Device Identity and optionally change the name of the Root CA, you will get a download zip file:

Opening the Zip file, you will see a text file with the name, the private and public key of the issued device and the public key of the issuing authority.

For this demonstration site, I am using SSL to interact with the web page and the URL requires an authorization key. Want to see it in action? Try this URL: https://certforiotdemo.azurewebsites.net/api/certificate?code=kLkJun5XWTqa8hIKZIUzmonqS4c0USUFaQkTmNwe7pM5qvMg2ylQng==.

To call the API directly, consider the following URL. Note the action, cn rootName and code parameters.

https://certforiotdemo.azurewebsites.net/api/certificate?action=createCertificate&cn=mysupercooldevice&rootName=MyRootCA&code=kLkJun5XWTqa8hIKZIUzmonqS4c0USUFaQkTmNwe7pM5qvMg2ylQng==

Background Information:

For this solution, I used OpenSSL, which is super simple to install on Linux. The Azure Functions that run on Linux already have it installed. If I wanted to use an Azure Function on Windows, I would have had to create a custom image with OpenSSL installed.

This website is just a demonstration. For production, I would either store the certificates in a separate Azure Storage Account or use Azure Key Vault.

Lastly, Node.JS is pretty simple and does have some OpenSSL libraries, such as https://www.npmjs.com/package/pem, but in the end I found it simpler to just run openssl commands natively.

Code:

The following Node.JS code was used for the entire solution (from https://github.com/ksaye/simpleCAforIoT):

var fs = require('fs');
var camelcase = require('camelcase');
var JSZip = require("jszip");
const {execSync} = require('child_process');

module.exports = async function (context, req) {
    if (req.query.action && (req.query.action == "createCertificate" && req.query.cn) || (req.query.action == "getRootCertificate")) {
        var certOptions = {
            serial: Math.floor(Math.random() * 1000000000),     // not using a sequential serial number
            days: 365 * 100                                     // 100 year certificates
        };

        var rootName = camelcase("RootAuthority");              // default name of our CA, unless given via a parameter
        if (req.query.rootName){
            rootName = camelcase(req.query.rootName);
        }

        if(!fs.existsSync(rootName + '-publicKey.pem') || !fs.existsSync(rootName + '-privateKey.pem')){     // no CA by that name, creating one.
            execSync('openssl genrsa 2048 > ' + rootName + '-privateKey.pem');
            execSync('openssl req -new -x509 -config config.txt -nodes -days ' + certOptions.days + ' -key ' + rootName + '-privateKey.pem -out ' + rootName + '-publicKey.pem -subj "/CN=' + rootName + '"');
        }

        if (req.query.action == "createCertificate" && req.query.cn){   // create a client cert and return the ZIP file of the private, public and Issuing Authority public key
            var deviceId = camelcase(req.query.cn);
            if (req.query.iotedge){                                     // for Azure IoT Edge the Certificate must have the CA capability
                execSync('openssl req -newkey rsa:2048 -days ' + certOptions.days + ' -nodes -keyout ' + deviceId + '-privateKey.pem -out ' + deviceId + '-request.pem -subj "/CN=' + deviceId + '"' + 
                ' --addext basicConstraints=critical,CA:TRUE,pathlen:2 --addext keyUsage=keyCertSign,digitalSignature');
            } else {
                execSync('openssl req -newkey rsa:2048 -days ' + certOptions.days + ' -nodes -keyout ' + deviceId + '-privateKey.pem -out ' + deviceId + '-request.pem -subj "/CN=' + deviceId + '"');
            }
            execSync('openssl rsa -in ' + deviceId + '-privateKey.pem -out ' + deviceId + '-privateKey.pem');
            execSync('openssl x509 -req -in ' + deviceId + '-request.pem -days ' + certOptions.days + ' -CA ' + rootName +
                '-publicKey.pem -CAkey ' + rootName + '-privateKey.pem -set_serial ' + certOptions.serial + ' -out ' + deviceId + '-publicKey.pem');

            await new Promise((resolve, reject) => {
                var zipFile = new JSZip();
                zipFile.file(deviceId + '-publicKey.pem', fs.readFileSync(deviceId + '-publicKey.pem'));        // public key
                zipFile.file(deviceId + '-privateKey.pem', fs.readFileSync(deviceId + '-privateKey.pem'));      // private key
                zipFile.file(rootName + '-publicKey.pem', fs.readFileSync(rootName + '-publicKey.pem'));        // public key of the CA
                zipFile.file('certificateIdentity.txt', deviceId);                                              // CN name in the certificate for some clients
                zipFile.generateNodeStream({type:'nodebuffer',streamFiles:true})
                    .pipe(fs.createWriteStream(deviceId + '.zip'))
                    .on('finish', function () {
                        resolve();
                    });
            });

            var data = fs.readFileSync(deviceId + '.zip');
            let headers = {
                'Content-Type': 'application/zip',
                'Content-disposition': 'attachment;filename=' + deviceId + '.zip',
                'Content-Length': data.length
            };
            context.res = {
                status: 200,
                headers: headers,
                isRaw: true,
                body: data
            };

        } else if (req.query.action == "getRootCertificate"){       // return the root certificate Public Key
            var data = fs.readFileSync(rootName + '-publicKey.pem');
            let headers = {
                'Content-Type': 'application/x-x509-ca-cert',
                'Content-disposition': 'attachment;filename=' + rootName + '_cert.pem',
                'Content-Length': data.length
            };
            context.res = {
                status: 200,
                headers: headers,
                isRaw: true,
                body: data
            };
        } 
    } else {
        let headers = {
            'Content-Type': 'text/html'
        };
        context.res = {
            status: 200,
            headers: headers,
            isRaw: false,
            body: '<HTML><HEAD></HEAD><BODY><center><H1>Create x509 Certificate</H1><br>' +
            '<FORM action="./' + context.executionContext.functionName + '">Root CA Name:<input type="text" value="RootAuthority" name="rootName"><br>' +
            'Device Identity:<input type="text" name="cn"><br>' +
            'Is Azure IoT Edge:<input type="checkbox" name="iotedge"><i>subordinate CA setting</i><br>' +
            '<input type="hidden" name="action" value="createCertificate">' +
            '<input type="hidden" name="code" value="' + req.query.code + '">' +
            '<input type="submit" value="Submit Certificate Request">' +
            '</FORM></center></BODY></HTML>'
        };
    }
};

Setup:

Use the following sets to setup the Azure Function and import the code from GitHub.

  1. Clone the repository by running:
git clone https://github.com/ksaye/simpleCAforIoT.git

2. Open the SimpleCAForIoT directory in Visual Studio Code, right click on the certificate directory and Deploy to Function App (required the Azure Function Extension in Visual Studio Code):

3. In VS Code, select create new Function App in Azure:

4. Name your Function App:

5. Select the Node Runtime:

6. Select the Linux OS:

7. Select the Region you want to deploy this Function App:

8. Finally, the solution will be deployed:

9. Lastly, open the Azure Portal to get the URL of your application, which will include the authentication code:

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s