4
votes

I am using NodeJS along with AWS JS SDK and AWS IoT Device JS SDK in order to automatically create a new thing and assign certificates and policies to it once it connects to my server.

I was following "Just-in-Time Registration" article in order to create, register and activate my CA certificate. As far as I can tell, the CA certificate is successfully added to AWS IoT, activated and enabled for auto-registration.

What I don't understand is how is this step performed (quote from mentioned article):

When a device attempts to connect with an X.509 certificate that is not known to AWS IoT but was signed by a CA that was registered with AWS IoT, the device certificate will be auto-registered by AWS IoT in a new PENDING_ACTIVATION state.

How do I make an "attempt" to connect? Since I was using aws-iot-device-sdk-js SDK, with manually created certificates, I was usually connecting my device like this:

const device = deviceModule.device({
  host: 'myendpoint.iot.us-east-1.amazonaws.com',
  region: 'us-east-1',
  keyPath: `certs/${deviceID}.key`,
  certPath: `certs/${deviceID}.pem`,
  caPath: 'certs/rootCA.pem',
  clientId: deviceID,
  baseReconnectTimeMs: 4000,
  keepalive: 30,
  protocol: 'mqtts',
});

But now I don't have certificate and key to include in keyPath and certPath and I cannot instantiate my device without it.

I tried to create certificates myself, using createKeysAndCertificate() from AWS SDK, saving them to disk, attaching a policy manually, attaching a principal manually even tried to mark certificate as "active" manually, something along these lines:

iot.createThing({ thingName: deviceID }, (err, d) => {
  if (err) {
    console.log(err);
  } else {
    allThings[d.thingName] = d;
    iot.createKeysAndCertificate({ setAsActive: true }, (e, c) => {
      if (e) {
        console.log(e);
      } else {
        fs.writeFile(`certs/${deviceID}.pem`, c.certificatePem, (ef, f) => {
          if (ef) throw ef;
        });
        fs.writeFile(`certs/${deviceID}.key`, c.keyPair.PrivateKey, (ef, f) => {
          if (ef) throw ef;
        });
        iot.attachPrincipalPolicy({
          policyName: 'my-testing-policy',
          principal: c.certificateArn,
        }, (ee, cc) => {
          if (ee) {
            console.log(ee);
          } else {
            iot.attachThingPrincipal({
              principal: c.certificateArn,
              thingName: deviceID,
            }, (prerr, prdata) => {
              if (prerr) {
                console.log(prerr);
              } else {
                iot.acceptCertificateTransfer({
                  certificateId: c.certificateId,
                  setAsActive: true,
                }, (ce, cd) => {
                  if (err) {
                    console.log(err);
                  } else {
                    console.log('cert activated.');
                  }
                });
              }
            });
          }
        });
      }
    });
  }
});

But after all this, when I try to publish something I am presented with an error:

Error: unable to get local issuer certificate
    at Error (native)
    at TLSSocket.<anonymous> (_tls_wrap.js:1092:38)
    at emitNone (events.js:86:13)
    at TLSSocket.emit (events.js:185:7)
    at TLSSocket._finishInit (_tls_wrap.js:610:8)
    at TLSWrap.ssl.onhandshakedone (_tls_wrap.js:440:38)

I also tried to subscribe to specific topic, as mentioned in the same article above, aws/events/certificates/registered/e3f0a30... but I've never seen a single message on that topic...

What am I missing here? How do I trigger device certificate and private key generation properly just by using my Just-in-Time certificate?

1
First, you must use your own CA, with it you can generate your own certificate for your device. You have to register this CA in AWS IoT. When AWS IoT encounter a new certificate that has been issued by this CA, it will put the device in PENDING_ACTIVATION state. You have to create your own certificate yourself, you don't have to use the AWS SDK for this. Here the documentation for that docs.aws.amazon.com/iot/latest/developerguide/…. AWS IoT is tricky, there's a lot to understandseverin.julien
I did create my own CA, I can see it in my IoT console, it has status "ACTIVATED" and "Auto-registration" is set to "ENABLE". How should I use it now to create my device key and certificate once the device connects to my server? I would like to avoid calling OS commands from NodeJS if it is possible to do it with AWS SDK somehow...errata
@Sapher the link you shared... So, I did everything to "Using Automatic/Just-in-Time Registration for Device Certificates" part... If you look at that paragraph, you will also see the sentence: "When a device first attempts to connect to AWS IoT, as part of the TLS handshake, it must present a registered CA certificate and a device certificate." I never managed to do this... What does "attempt to connect" means? Publishing a message or what?? How do I present "device certificate" if I don't create one myself? I thought that this root CA should do it for me?errata
Your issue is not big, don't worry :). The issue is that you have to generate your own certificate, and for that you have to use openssl, there's surely others way to do it, maybe there's a magic function to do it in AWS-SDK, but the way I know is trough openssl. In the link I gave you, the part you need is "Creating a Device Certificate Using Your CA Certificate". "Attempt to connect" mean that your device try to connect to amazon, if Amazon know about your CA, it will know that this new certificate he doesn't know, come from your CA.severin.julien
@Sapher Oh, alright, so I do have to create device key and cert manually? And this device cert should be in "PENDING" state then? Can you give me an example of the device's "connection attempt" in any language please? JS, Java, Python, bash, whatever :)errata

1 Answers

4
votes

when you want to use your own CA you need to create the certificates with this CA and using openssl. It cant be made by using AWS SDK because you need to use your private rootCA key and AWS has not this main element and aws shouldnt have it. Using your own CA is meaned for mass production, otheway you could use the AWS SDK to create certs with AWS' rootCA.

Using just in time registration

How Just in time registration works?

  1. You need to create your own private rootCA key and only you should have it. You must to protect it due security. You create a cert of this private Key and it is AWS IoT use to identify the ccerts created by your private rootCA.
  2. Since you need to use openssl to create certs for your devices you should use OS instructions or find a library that can do this on NodeJS level. Here is the sample to create your certs by using OS bash script:

    mkdir /iot/certsTemp cd /iot/certsTemp openssl genrsa -out $1.key 2048 openssl req -new -key $1.key -out $1.csr -subj "/C=MX/ST=CDMX/L=CDMX/O=CompanyX/OU=IoT/CN=IoT" openssl x509 -req -in $1.csr -CA /iot/CACerts/CACertificate.pem -CAkey /iot/CACerts/CACertificate.key -CAcreateserial -out $1.crt -days 1850 -sha256 cat $1.crt /iot/CACerts/CACertificate.pem > $1-CA.crt cat $1.crt

And this instructions can be invoked in NodeJs by using something like this:

var sh = spawn('sh', ['bash/generateDeviceCerts.sh', deviceId]);
var pem;
sh.stdout.on('data', (data) => {
    if (data.indexOf('-----BEGIN CERTIFICATE-----') == 0) {
        pem = data.toString();
    }
});
sh.stderr.on('data', (data) => {
    //console.log(`cert stderr: ${data}`);
});
sh.on('close', (code) => {
    if (code == 0) {
        me.uploadCertificatesToS3(deviceId);
    }
});
  1. Once you have this certs you can use them just 1 pair per device. The first time the device try to connect, AWS detects the certs were created by using the rootCA of the user and you can catch this event with IoT Rule engine listen to a topic and invoking a Lambda. The last part of the topic is your rootCA cert id in the AWS IoT

The last part of the topic is your rootCA cert id in the AWS IoT

And the lambda looks like:

var AWS = require('aws-sdk');

exports.handler = function(event, context, callback) {

var region = "us-east-1";

var accountId = event.awsAccountId.toString().trim();

var iot = new AWS.Iot({
    'region': region,
    apiVersion: '2015-05-28'
});
var certificateId = event.certificateId.toString().trim();

var certificateARN = `arn:aws:iot:${region}:${accountId}:cert/${certificateId}`;
var policyName = < Policy name > ;
//Asign IoT Policy to certificate
iot.attachPrincipalPolicy({
    policyName: policyName,
    principal: certificateARN
}, (err, data) => {
    if (err && (!err.code || err.code !== 'ResourceAlreadyExistsException')) {
        callback(err, data);
        return;
    }
    //Active the certificate
    iot.updateCertificate({
        certificateId: certificateId,
        newStatus: 'ACTIVE'
    }, (err, data) => {
        if (err) callback(err, data);
        else iot.createThing(params, function(err, data) {//Create a thing for this certificate
            if (err) callback(err, null);
            else {
                var params = {
                    principal: certificateARN,
                    thingName: <NAME>
                };
                //Attach certificate to this thing
                iot.attachThingPrincipal(params, function(err, data) {
                    if (err) callback(err, null);
                    else callback(null, "SUCCESS");
                });
            }
        });
    });
});
}

And this is all. The first time the device try to connect it will fail but will run this script in lambda, once it finish successfully the next time the device try to connect it should work because the certificate is registered and active.

Using SDK and Lambda.

If you dont have a mass production devices i recommend to use SDK and Lambda to do this, the lambda code is something like this:

var AWS = require('aws-sdk');

exports.handler = function(event, context, callback) {

const region = "us-east-1";
const accountId = < YOUR ACCOUNT ID > ;
const BUCKET = < YOUR BUCKET > ;

var certificateARN;

var iot = new AWS.Iot({
    'region': region,
    apiVersion: '2015-05-28'
});

event.data.id = getId();
iot.createKeysAndCertificate({
    setAsActive: true
}, function(err, data) {
    if (err) callback(err, null);
    else {
        certificateARN = `arn:aws:iot:${region}:${accountId}:cert/${data.certificateId}`;
        uploadCertificatesToS3(event.data.id, data);
        iot.attachPrincipalPolicy({
            policyName: < YOUR IOT POLICY NAME > ,
            principal: certificateARN
        }, (err, data) => {
            if (err) callback(err, data);
            else {
                var thingName = event.data.id;
                iot.createThing({
                    thingName: thingName,
                    attributePayload: {
                        attributes: {},
                        merge: false
                    }
                }, function(err, data) {
                    if (err) callback(err, null);
                    else {
                        iot.attachThingPrincipal({
                            principal: certificateARN,
                            thingName: thingName
                        }, function(err, data) {
                            if (err) callback(err, null);
                            else callback(null, event.data);
                        });
                    }
                });
            }
        });
    }
});

function getId() {
    return Math.trunc(new Date().getTime() / 1000) + '-' + Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
}

function uploadCertificatesToS3(deviceId, certsData) {
    return Promise.all([
        new Promise(function(resolve, reject) {
            var base64data = new Buffer(certsData.certificatePem, 'binary');
            var s3 = new AWS.S3();
            s3.putObject({
                Bucket: BUCKET,
                Key: `${event.data.project}/devices-certificates/${deviceId}/${deviceId}.crt`,
                Body: base64data
            }, function(err, data) {
                if (err) console.log("Error S3", deviceId);
            });
        }),
        new Promise(function(resolve, reject) {
            var base64data = new Buffer(certsData.keyPair.PrivateKey, 'binary');
            var s3 = new AWS.S3();
            s3.putObject({
                Bucket: BUCKET,
                Key: `${event.data.project}/devices-certificates/${deviceId}/${deviceId}.key`,
                Body: base64data
            }, function(err, data) {
                if (err) console.log("Error S3", deviceId);
            });
        }),
        new Promise(function(resolve, reject) {
            var base64data = new Buffer(certsData.keyPair.PublicKey, 'binary');
            var s3 = new AWS.S3();
            s3.putObject({
                Bucket: BUCKET,
                Key: `${event.data.project}/devices-certificates/${deviceId}/${deviceId}.public.key`,
                Body: base64data
            }, function(err, data) {
                if (err) console.log("Error S3", deviceId);
            });
        })
    ]);
}
}

This will create the certs using the AWS rootCA, save them in s3, create the thing, attach policy to certs and attach certs to the thing, all in just one lambda function. You can invoke this lambda by using somthing like this:

var AWS = require("aws-sdk");
AWS.config.update({
   region: "us-east-1"
});

new AWS.Lambda().invoke({
    FunctionName: 'createDevice',
    InvocationType: "RequestResponse",
    LogType: "Tail",
    Payload: JSON.stringify({
        data: {
            name: < NAME > ,
            description: < DESCRIPTION > ,
            project: < PROJECT >
        }
    })
}, function(err, data) {
    if (err) console.log(err);
    else console.log(JSON.parse(data.Payload));
});

In" RequestResponse" lambda execution mode the callback will be executed when you call "callback" function in the lambda and you can receive data from that lambda.

Suggestions You shoul create IoT policies with restrictions and to use all the elements I have described here you need to configure the correct permissions.

Regards,