From 6c7e4086ef24be420639620e9a229c038e5a269f Mon Sep 17 00:00:00 2001 From: Markus Tacker Date: Fri, 29 Nov 2019 12:29:55 +0100 Subject: [PATCH] feat(iot): allow multiple enrollment groups --- arm/resources.ts | 8 ++ cli/bifravst.ts | 35 +++-- cli/commands/connect.ts | 3 +- ...nerate-cert.ts => generate-device-cert.ts} | 27 ++-- cli/commands/proof-ca-possession.ts | 24 ++-- cli/commands/register-ca-intermediate.ts | 71 ++++++++++ cli/commands/register-ca-root.ts | 81 ++++++++++++ cli/commands/register-ca.ts | 125 ------------------ cli/index.js | 2 +- cli/iot/caFileLocations.ts | 20 ++- cli/iot/generateCA.ts | 94 ------------- cli/iot/generateCAIntermediate.ts | 56 ++++++++ cli/iot/generateCARoot.ts | 60 +++++++++ cli/iot/generateDeviceCertificate.ts | 40 ++---- cli/iot/generateProofOfPosession.ts | 19 +-- cli/iot/intermediateRegistry.ts | 32 +++++ cli/iot/pemConfig.ts | 20 +++ cli/logging.ts | 9 ++ package-lock.json | 9 ++ package.json | 4 +- 20 files changed, 441 insertions(+), 298 deletions(-) create mode 100644 arm/resources.ts rename cli/commands/{generate-cert.ts => generate-device-cert.ts} (58%) create mode 100644 cli/commands/register-ca-intermediate.ts create mode 100644 cli/commands/register-ca-root.ts delete mode 100644 cli/commands/register-ca.ts delete mode 100644 cli/iot/generateCA.ts create mode 100644 cli/iot/generateCAIntermediate.ts create mode 100644 cli/iot/generateCARoot.ts create mode 100644 cli/iot/intermediateRegistry.ts create mode 100644 cli/iot/pemConfig.ts create mode 100644 cli/logging.ts diff --git a/arm/resources.ts b/arm/resources.ts new file mode 100644 index 0000000..9becdfd --- /dev/null +++ b/arm/resources.ts @@ -0,0 +1,8 @@ +export const resourceGroupName = () => 'bifravst' + +export const deploymentName = resourceGroupName + +/** + * Returns the name of the Device Provisioning Service + */ +export const iotDeviceProvisioningServiceName = () => `${resourceGroupName()}ProvisioningService` \ No newline at end of file diff --git a/cli/bifravst.ts b/cli/bifravst.ts index 98ac526..b3167a5 100644 --- a/cli/bifravst.ts +++ b/cli/bifravst.ts @@ -1,14 +1,16 @@ import * as program from 'commander' import chalk from 'chalk' import * as path from 'path' -import { registerCaCommand } from './commands/register-ca' +import { registerCARootCommand } from './commands/register-ca-root' import { IotHubClient } from "@azure/arm-iothub"; import { IotDpsClient } from '@azure/arm-deviceprovisioningservices' import { AzureCliCredentials } from "@azure/ms-rest-nodeauth"; -import { generateDeviceCommand } from './commands/generate-cert'; +import { generateDeviceCommand } from './commands/generate-device-cert'; import { connectCommand } from './commands/connect'; import { run } from './process/run'; -import { proofCaPossessionCommand } from './commands/proof-ca-possession'; +import { proofCARootPossessionCommand } from './commands/proof-ca-possession'; +import { registerCAIntermediateCommand } from './commands/register-ca-intermediate'; +import { iotDeviceProvisioningServiceName, resourceGroupName, deploymentName } from '../arm/resources'; const ioTHubDPSConnectionString = ({ deploymentName, resourceGroupName }: { deploymentName: string, resourceGroupName: string }) => async () => (await run({ command: 'az', @@ -37,20 +39,33 @@ const getCurrentCreds = () => { const bifravstCLI = async () => { const certsDir = path.resolve(process.cwd(), 'certificates') - const resourceGroupName = 'bifravst' - const deploymentName = 'bifravst' + const resourceGroup = resourceGroupName() + const deployment = deploymentName() + const dpsName = iotDeviceProvisioningServiceName() - const getIotHubConnectionString = ioTHubDPSConnectionString({ resourceGroupName, deploymentName }) + const getIotHubConnectionString = ioTHubDPSConnectionString({ resourceGroupName: resourceGroup, deploymentName: deployment }) const getIotDpsClient = () => getCurrentCreds().then(creds => new IotDpsClient(creds, creds.tokenInfo.subscription)) const getIotClient = () => getCurrentCreds().then(creds => new IotHubClient(creds, creds.tokenInfo.subscription)) program.description('Bifravst Command Line Interface') const commands = [ - registerCaCommand({ + registerCARootCommand({ certsDir, - ioTHubDPSConnectionString: getIotHubConnectionString, iotDpsClient: getIotDpsClient, + dpsName, + resourceGroup + }), + proofCARootPossessionCommand({ + iotDpsClient: getIotDpsClient, + certsDir, + dpsName, + resourceGroup + }), + registerCAIntermediateCommand({ + certsDir, + ioTHubDPSConnectionString: getIotHubConnectionString, + iotDpsClient: getIotDpsClient }), generateDeviceCommand({ iotClient: getIotClient, @@ -60,10 +75,6 @@ const bifravstCLI = async () => { iotDpsClient: getIotDpsClient, certsDir }), - proofCaPossessionCommand({ - iotDpsClient: getIotDpsClient, - certsDir - }) ] let ran = false diff --git a/cli/commands/connect.ts b/cli/commands/connect.ts index 121d01f..263be91 100644 --- a/cli/commands/connect.ts +++ b/cli/commands/connect.ts @@ -60,10 +60,11 @@ export const connectCommand = ({ }) })) + iotHub = registry.iotHub + console.log(chalk.magenta(`Device registration succeeded with IotHub`), chalk.yellow(iotHub)) await fs.writeFile(deviceFiles.registry, JSON.stringify(registry, null, 2), 'utf-8') - iotHub = registry.iotHub } finally { const connection = Client.fromConnectionString(`HostName=${iotHub};DeviceId=${deviceId};x509=true`, MqttDevice); diff --git a/cli/commands/generate-cert.ts b/cli/commands/generate-device-cert.ts similarity index 58% rename from cli/commands/generate-cert.ts rename to cli/commands/generate-device-cert.ts index 7df6964..4f1f186 100644 --- a/cli/commands/generate-cert.ts +++ b/cli/commands/generate-device-cert.ts @@ -3,6 +3,8 @@ import { ComandDefinition } from './CommandDefinition' import { randomWords } from '@bifravst/random-words' import { generateDeviceCertificate } from '../iot/generateDeviceCertificate' import { IotHubClient } from "@azure/arm-iothub"; +import { log, debug } from '../logging' +import { list as listIntermediateCerts } from '../iot/intermediateRegistry' export const generateDeviceCommand = ({ certsDir, @@ -10,25 +12,34 @@ export const generateDeviceCommand = ({ iotClient: () => Promise, certsDir: string }): ComandDefinition => ({ - command: 'generate-cert', + command: 'generate-device-cert', options: [ { flags: '-d, --deviceId ', description: 'Device ID, if left blank a random ID will be generated', }, + { + flags: '-i, --intermediateCertId ', + description: 'ID of the CA intermediate certificate to use, if left blank the first will be used', + }, ], - action: async ({ deviceId }: { deviceId: string }) => { + action: async ({ deviceId, intermediateCertId }: { deviceId: string, intermediateCertId: string }) => { const id = deviceId || (await randomWords({ numWords: 3 })).join('-') + if (!intermediateCertId) { + const intermediateCerts = await listIntermediateCerts({ certsDir }) + intermediateCertId = intermediateCerts[0] + } + + console.log(chalk.magenta('Intermediate certificate:'), chalk.yellow(intermediateCertId)) + await generateDeviceCertificate({ deviceId: id, certsDir, - log: (...message: any[]) => { - console.log(...message.map(m => chalk.magenta(m))) - }, - debug: (...message: any[]) => { - console.log(...message.map(m => chalk.cyan(m))) - }, + log, + debug, + intermediateCertId + }) console.log( chalk.magenta(`Certificate for device ${chalk.yellow(id)} generated.`), diff --git a/cli/commands/proof-ca-possession.ts b/cli/commands/proof-ca-possession.ts index ae26849..c58f946 100644 --- a/cli/commands/proof-ca-possession.ts +++ b/cli/commands/proof-ca-possession.ts @@ -2,27 +2,29 @@ import chalk from 'chalk' import { ComandDefinition } from './CommandDefinition' import { IotDpsClient } from '@azure/arm-deviceprovisioningservices' import { promises as fs } from 'fs' -import { caFileLocations } from '../iot/caFileLocations' +import { CARootFileLocations } from '../iot/caFileLocations' -export const proofCaPossessionCommand = ({ +export const proofCARootPossessionCommand = ({ certsDir, iotDpsClient, + resourceGroup, + dpsName, }: { certsDir: string + resourceGroup: string + dpsName: string iotDpsClient: () => Promise }): ComandDefinition => ({ - command: 'proof-ca-possession', + command: 'proof-ca-root-possession', action: async () => { - const certificateName = 'bifravst-root' - const resourceGroupName = 'bifravst' - const dpsName = 'bifravstProvisioningService' + const certLocations = CARootFileLocations(certsDir) - const armDpsClient = await iotDpsClient() + const certificateName = (await fs.readFile(certLocations.name, 'utf-8')).trim() - const { etag } = await armDpsClient.dpsCertificate.get(certificateName, resourceGroupName, dpsName) + const armDpsClient = await iotDpsClient() - const certLocations = caFileLocations(certsDir) + const { etag } = await armDpsClient.dpsCertificate.get(certificateName, resourceGroup, dpsName) const verificationCert = await fs.readFile(certLocations.verificationCert, 'utf-8') @@ -30,11 +32,11 @@ export const proofCaPossessionCommand = ({ await armDpsClient.dpsCertificate.verifyCertificate(certificateName, etag as string, { certificate: verificationCert, - }, resourceGroupName, dpsName) + }, resourceGroup, dpsName) console.log(chalk.magenta('Verified root CA certificate.')) console.log() - console.log(chalk.green('You can now generate device certificates using'), chalk.blueBright('node cli generate-cert')) + console.log(chalk.green('You can now register a CA intermediate certificate using'), chalk.blueBright('node cli register-ca-intermediate')) }, help: 'Verifies the root CA certificate which is registered with the Device Provisioning System', }) diff --git a/cli/commands/register-ca-intermediate.ts b/cli/commands/register-ca-intermediate.ts new file mode 100644 index 0000000..d7e1c2f --- /dev/null +++ b/cli/commands/register-ca-intermediate.ts @@ -0,0 +1,71 @@ +import chalk from 'chalk' +import { ComandDefinition } from './CommandDefinition' +import { generateCAIntermediate } from '../iot/generateCAIntermediate' +import { ProvisioningServiceClient } from 'azure-iot-provisioning-service' +import { IotDpsClient } from '@azure/arm-deviceprovisioningservices' +import { add as addToIntermediateRegistry } from '../iot/intermediateRegistry' +import { v4 } from 'uuid' +import { log, debug } from '../logging' + +export const registerCAIntermediateCommand = ({ + certsDir, + ioTHubDPSConnectionString, +}: { + certsDir: string + ioTHubDPSConnectionString: () => Promise + iotDpsClient: () => Promise +}): ComandDefinition => ({ + command: 'register-ca-intermediate', + action: async () => { + + const id = v4() + + const intermediate = await generateCAIntermediate({ + id, + certsDir, + log, + debug + }) + console.log(chalk.magenta(`CA intermediate certificate generated.`)) + + await addToIntermediateRegistry({ certsDir, id }) + + // Create enrollment group + + const dpsConnString = await ioTHubDPSConnectionString() + + const dpsClient = ProvisioningServiceClient.fromConnectionString(dpsConnString) + + const enrollmentGroupId = `bifravst-${id}` + + await dpsClient.createOrUpdateEnrollmentGroup({ + enrollmentGroupId, + attestation: { + type: 'x509', + //@ts-ignore + x509: { + signingCertificates: { + primary: { + certificate: intermediate.certificate + } + } + } + }, + provisioningStatus: "enabled", + reprovisionPolicy: { + migrateDeviceData: true, + updateHubAssignment: true + } + }) + + console.log( + chalk.magenta(`Created enrollment group for CA intermediate certificiate`), + chalk.yellow(enrollmentGroupId) + ) + + console.log() + + console.log(chalk.green('You can now generate device certificates using'), chalk.blueBright('node cli generate-device-cert')) + }, + help: 'Creates a CA intermediate certificate registers it with an IoT Device Provisioning Service enrollment group', +}) diff --git a/cli/commands/register-ca-root.ts b/cli/commands/register-ca-root.ts new file mode 100644 index 0000000..984805c --- /dev/null +++ b/cli/commands/register-ca-root.ts @@ -0,0 +1,81 @@ +import chalk from 'chalk' +import { ComandDefinition } from './CommandDefinition' +import { IotDpsClient } from '@azure/arm-deviceprovisioningservices' +import { generateProofOfPosession } from '../iot/generateProofOfPosession' +import { v4 } from 'uuid' +import { generateCARoot } from '../iot/generateCARoot' +import { log, debug } from '../logging' + +export const registerCARootCommand = ({ + certsDir, + iotDpsClient, + resourceGroup, + dpsName, +}: { + certsDir: string + resourceGroup: string + dpsName: string + iotDpsClient: () => Promise +}): ComandDefinition => ({ + command: 'register-ca-root', + action: async () => { + const certificateName = `bifravst-root-${v4()}` + + const root = await generateCARoot({ + certsDir, + name: certificateName, + log, + debug + }) + console.log(chalk.magenta(`CA root certificate generated.`)) + + // Register root CA certificate on DPS + + const armDpsClient = await iotDpsClient() + + await armDpsClient.dpsCertificate.createOrUpdate( + resourceGroup, + dpsName, + certificateName, + { + certificate: root.certificate + }, + ) + + console.log( + chalk.magenta(`CA root registered with DPS.`), + chalk.yellow(dpsName) + ) + + // Create verification cert + + const { etag } = await armDpsClient.dpsCertificate.get(certificateName, resourceGroup, dpsName) + const { properties } = await armDpsClient.dpsCertificate.generateVerificationCode( + certificateName, + etag as string, + resourceGroup, + dpsName + ) + + if (!properties?.verificationCode) { + throw new Error(`Failed to generate verification code`) + } + + await generateProofOfPosession({ + certsDir, + log, + debug, + verificationCode: properties.verificationCode + }) + + console.log( + chalk.magenta(`Generated verification certificate for verification code`), + chalk.yellow(properties.verificationCode) + ) + + console.log() + + console.log(chalk.green('You can now verify the proof of posession using'), chalk.blueBright('node cli proof-ca-root-possession')) + }, + help: 'Creates a CA root certificate and registers it with the IoT Device Provisioning Service', +}) diff --git a/cli/commands/register-ca.ts b/cli/commands/register-ca.ts deleted file mode 100644 index ac71f81..0000000 --- a/cli/commands/register-ca.ts +++ /dev/null @@ -1,125 +0,0 @@ -import chalk from 'chalk' -import { ComandDefinition } from './CommandDefinition' -import { generateCAChain } from '../iot/generateCA' -import { ProvisioningServiceClient } from 'azure-iot-provisioning-service' -import { IotDpsClient } from '@azure/arm-deviceprovisioningservices' -import { generateProofOfPosession } from '../iot/generateProofOfPosession' - -export const registerCaCommand = ({ - certsDir, - ioTHubDPSConnectionString, - iotDpsClient, -}: { - certsDir: string - ioTHubDPSConnectionString: () => Promise - iotDpsClient: () => Promise -}): ComandDefinition => ({ - command: 'register-ca', - action: async () => { - - const log = (...message: any[]) => { - console.log(...message.map(m => chalk.magenta(m))) - } - - const debug = (...message: any[]) => { - console.log(...message.map(m => chalk.cyan(m))) - } - - const { root, intermediate } = await generateCAChain({ - certsDir, - log, - debug - }) - console.log(chalk.magenta(`CA root and intermediate certificate generated.`)) - - // Register root CA certificate on DPS - - const certificateName = 'bifravst-root' - const resourceGroupName = 'bifravst' - const dpsName = 'bifravstProvisioningService' - - const armDpsClient = await iotDpsClient() - - await armDpsClient.dpsCertificate.createOrUpdate( - resourceGroupName, - dpsName, - certificateName, - { - certificate: root.certificate - }, - ) - - console.log( - chalk.magenta(`CA root registered with DPS.`), - chalk.yellow(dpsName) - ) - - // Create verification cert - - const { etag } = await armDpsClient.dpsCertificate.get(certificateName, resourceGroupName, dpsName) - const { properties } = await armDpsClient.dpsCertificate.generateVerificationCode( - certificateName, - etag as string, - resourceGroupName, - dpsName - ) - - if (!properties?.verificationCode) { - throw new Error(`Failed to generate verification code`) - } - - await generateProofOfPosession({ - certsDir, - log, - debug, - verificationCode: properties.verificationCode - }) - - console.log( - chalk.magenta(`Generated verification certificate for verification code`), - chalk.yellow(properties.verificationCode) - ) - - // Create enrollment group - - const dpsConnString = await ioTHubDPSConnectionString() - - const dpsHostname = dpsConnString.split(';')[0].split('=')[1] - - const dpsClient = ProvisioningServiceClient.fromConnectionString(dpsConnString) - - const enrollmentGroupId = 'bifravst' - - await dpsClient.createOrUpdateEnrollmentGroup({ - enrollmentGroupId, - attestation: { - type: 'x509', - //@ts-ignore - x509: { - signingCertificates: { - primary: { - certificate: intermediate.certificate - } - } - } - }, - provisioningStatus: "enabled", - reprovisionPolicy: { - migrateDeviceData: true, - updateHubAssignment: true - } - }) - - console.log( - chalk.magenta(`Created enrollment group for intermediate CA cert.`), - chalk.yellow(enrollmentGroupId) - ) - - console.log(chalk.magenta(`Added CA certificate to Device Provisioning Service`), chalk.yellow(dpsHostname)) - - console.log() - - console.log(chalk.green('You can now verify the proof of posession using'), chalk.blueBright('node cli proof-ca-possession')) - }, - help: 'Creates a CA for devices and adds it to the registry', -}) diff --git a/cli/index.js b/cli/index.js index 5586464..5315599 100755 --- a/cli/index.js +++ b/cli/index.js @@ -1,4 +1,4 @@ #!/usr/bin/env node // eslint-disable-next-line -require('../dist/bifravst') +require('../dist/cli/bifravst') diff --git a/cli/iot/caFileLocations.ts b/cli/iot/caFileLocations.ts index bd5cf07..21c3dc7 100644 --- a/cli/iot/caFileLocations.ts +++ b/cli/iot/caFileLocations.ts @@ -1,10 +1,20 @@ import * as path from 'path' -export const caFileLocations = (certsDir: string) => ({ - rootCert: path.resolve(certsDir, 'CA.root.pem'), - rootPrivateKey: path.resolve(certsDir, 'CA.root.key'), - intermediatePrivateKey: path.resolve(certsDir, 'CA.intermediate.key'), - intermediateCert: path.resolve(certsDir, 'CA.intermediate.pem'), +export const CARootFileLocations = (certsDir: string) => ({ + name: path.resolve(certsDir, 'CA.root.name'), + cert: path.resolve(certsDir, 'CA.root.pem'), + privateKey: path.resolve(certsDir, 'CA.root.key'), verificationKey: path.resolve(certsDir, 'CA.verification.key'), verificationCert: path.resolve(certsDir, 'CA.verification.pem'), }) + +export const CAIntermediateFileLocations = ({ + certsDir, + id +}: { + certsDir: string, + id: string +}) => ({ + privateKey: path.resolve(certsDir, `CA.intermediate.${id}.key`), + cert: path.resolve(certsDir, `CA.intermediate.${id}.pem`), +}) \ No newline at end of file diff --git a/cli/iot/generateCA.ts b/cli/iot/generateCA.ts deleted file mode 100644 index fbb460e..0000000 --- a/cli/iot/generateCA.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { promises as fs } from 'fs' -import { caFileLocations } from './caFileLocations' -import { createCertificate, CertificateCreationResult } from 'pem' - -/** - * Generates a CA certificate chain - * @see https://github.com/Azure/azure-iot-sdk-node/blob/5a7cd40145575175b4a100bbc84758f8a87c6d37/provisioning/tools/create_test_cert.js - * @see http://busbyland.com/azure-iot-device-provisioning-service-via-rest-part-1/ - */ -export const generateCAChain = async (args: { - certsDir: string - log: (...message: any[]) => void - debug: (...message: any[]) => void -}): Promise<{ root: CertificateCreationResult, intermediate: CertificateCreationResult }> => { - const { certsDir, log, debug } = args - const caFiles = caFileLocations(certsDir) - try { - await fs.stat(certsDir) - } catch { - await fs.mkdir(certsDir) - debug(`Created ${certsDir}`) - } - - let certExists = false - try { - await fs.stat(caFiles.rootCert) - certExists = true - } catch { - // pass - } - if (certExists) { - throw new Error(`CA Certificate exists: ${caFiles.rootCert}!`) - } - - const config = (commonName: string) => [ - '[req]', - 'req_extensions = v3_req', - 'distinguished_name = req_distinguished_name', - 'x509_extensions = v3_ca', - '[req_distinguished_name]', - 'commonName = ' + commonName, - '[v3_req]', - 'basicConstraints = critical, CA:true' - ].join('\n') - - // Create the Root CA Cert - - const rootName = 'Bifravst Root CA' - - const rootCert = await new Promise((resolve, reject) => createCertificate({ - commonName: rootName, - serial: Math.floor(Math.random() * 1000000000), - days: 365, - selfSigned: true, - config: config(rootName) - }, (err, cert) => { - if (err) return reject(err) - resolve(cert) - })) - - await fs.writeFile(caFiles.rootCert, rootCert.certificate); - await fs.writeFile(caFiles.rootPrivateKey, rootCert.clientKey); - - log('Root CA Certificate', caFiles.rootCert) - debug(rootCert.certificate) - - // Create the intermediate CA cert (signed by the root) - - const intermediateName = 'Bifravst Intermediate CA' - - const intermediateCert = await new Promise((resolve, reject) => createCertificate({ - commonName: intermediateName, - serial: Math.floor(Math.random() * 1000000000), - days: 365, - selfSigned: true, - config: config(intermediateName), - serviceKey: rootCert.clientKey, - serviceCertificate: rootCert.certificate - }, (err, cert) => { - if (err) return reject(err) - resolve(cert) - })) - - log('Intermediate CA Certificate', caFiles.intermediateCert) - debug(intermediateCert.certificate) - - await fs.writeFile(caFiles.intermediateCert, intermediateCert.certificate); - await fs.writeFile(caFiles.intermediatePrivateKey, intermediateCert.clientKey); - - return { - root: rootCert, - intermediate: intermediateCert, - } -} diff --git a/cli/iot/generateCAIntermediate.ts b/cli/iot/generateCAIntermediate.ts new file mode 100644 index 0000000..6988942 --- /dev/null +++ b/cli/iot/generateCAIntermediate.ts @@ -0,0 +1,56 @@ +import { promises as fs } from 'fs' +import { CARootFileLocations, CAIntermediateFileLocations } from './caFileLocations' +import { createCertificate, CertificateCreationResult } from 'pem' +import { caCertConfig } from './pemConfig' + +/** + * Generates a CA intermediate certificate + * @see https://github.com/Azure/azure-iot-sdk-node/blob/5a7cd40145575175b4a100bbc84758f8a87c6d37/provisioning/tools/create_test_cert.js + * @see http://busbyland.com/azure-iot-device-provisioning-service-via-rest-part-1/ + */ +export const generateCAIntermediate = async (args: { + certsDir: string + id: string + log: (...message: any[]) => void + debug: (...message: any[]) => void +}): Promise => { + const { certsDir, log, debug, id } = args + const caRootFiles = CARootFileLocations(certsDir) + + // Create the intermediate CA cert (signed by the root) + + const caIntermediateFiles = CAIntermediateFileLocations({ + certsDir, + id + }) + + const [ + rootPrivateKey, + rootCert + ] = await Promise.all([ + fs.readFile(caRootFiles.privateKey, 'utf-8'), + fs.readFile(caRootFiles.cert, 'utf-8'), + ]) + + const intermediateName = `Bifravst Intermediate CA (${id})` + + const intermediateCert = await new Promise((resolve, reject) => createCertificate({ + commonName: intermediateName, + serial: Math.floor(Math.random() * 1000000000), + days: 365, + config: caCertConfig(intermediateName), + serviceKey: rootPrivateKey, + serviceCertificate: rootCert + }, (err, cert) => { + if (err) return reject(err) + resolve(cert) + })) + + log('Intermediate CA Certificate', caIntermediateFiles.cert) + debug(intermediateCert.certificate) + + await fs.writeFile(caIntermediateFiles.cert, intermediateCert.certificate); + await fs.writeFile(caIntermediateFiles.privateKey, intermediateCert.clientKey); + + return intermediateCert +} diff --git a/cli/iot/generateCARoot.ts b/cli/iot/generateCARoot.ts new file mode 100644 index 0000000..e2043fe --- /dev/null +++ b/cli/iot/generateCARoot.ts @@ -0,0 +1,60 @@ +import { promises as fs } from 'fs' +import { CARootFileLocations } from './caFileLocations' +import { createCertificate, CertificateCreationResult } from 'pem' +import { caCertConfig } from './pemConfig' + +/** + * Generates a CA Root certificate + * + * @see https://github.com/Azure/azure-iot-sdk-node/blob/5a7cd40145575175b4a100bbc84758f8a87c6d37/provisioning/tools/create_test_cert.js + * @see http://busbyland.com/azure-iot-device-provisioning-service-via-rest-part-1/ + */ +export const generateCARoot = async ({ certsDir, name, log, debug }: { + certsDir: string + name: string + log: (...message: any[]) => void + debug: (...message: any[]) => void +}): Promise => { + const caFiles = CARootFileLocations(certsDir) + try { + await fs.stat(certsDir) + } catch { + await fs.mkdir(certsDir) + debug(`Created ${certsDir}`) + } + + let certExists = false + try { + await fs.stat(caFiles.cert) + certExists = true + } catch { + // pass + } + if (certExists) { + throw new Error(`CA Root certificate exists: ${caFiles.cert}!`) + } + + // Create the Root CA Cert + + const rootCert = await new Promise((resolve, reject) => createCertificate({ + commonName: name, + serial: Math.floor(Math.random() * 1000000000), + days: 365, + selfSigned: true, + config: caCertConfig(name) + }, (err, cert) => { + if (err) return reject(err) + resolve(cert) + })) + + await Promise.all([ + fs.writeFile(caFiles.cert, rootCert.certificate, 'utf-8'), + fs.writeFile(caFiles.privateKey, rootCert.clientKey, 'utf-8'), + fs.writeFile(caFiles.name, name, 'utf-8') + ]) + + log('Root CA Certificate', caFiles.cert) + debug(rootCert.certificate) + + return rootCert +} diff --git a/cli/iot/generateDeviceCertificate.ts b/cli/iot/generateDeviceCertificate.ts index 71f5e3c..5b84602 100644 --- a/cli/iot/generateDeviceCertificate.ts +++ b/cli/iot/generateDeviceCertificate.ts @@ -1,8 +1,9 @@ import { promises as fs } from 'fs' -import { caFileLocations } from './caFileLocations' +import { CARootFileLocations, CAIntermediateFileLocations } from './caFileLocations' import { deviceFileLocations } from './deviceFileLocations' import * as os from 'os' import { createCertificate, CertificateCreationResult } from 'pem' +import { leafCertConfig } from './pemConfig' /** * Generates a certificate for a device, signed with the CA @@ -12,20 +13,17 @@ export const generateDeviceCertificate = async ({ log, debug, deviceId, + intermediateCertId, }: { certsDir: string deviceId: string + intermediateCertId: string log?: (...message: any[]) => void debug?: (...message: any[]) => void }): Promise<{ deviceId: string }> => { - try { - await fs.stat(certsDir) - } catch { - throw new Error(`${certsDir} does not exist.`) - } - log && log(`Generating certificate for device ${deviceId}`) - const caFiles = caFileLocations(certsDir) + const caRootFiles = CARootFileLocations(certsDir) + const caIntermediateFiles = CAIntermediateFileLocations({ certsDir, id: intermediateCertId }) const deviceFiles = deviceFileLocations({ certsDir, deviceId, @@ -33,31 +31,19 @@ export const generateDeviceCertificate = async ({ const [ intermediatePrivateKey, - intermediateCert + intermediateCert, + rootCert ] = await Promise.all([ - fs.readFile(caFiles.intermediatePrivateKey, 'utf-8'), - fs.readFile(caFiles.intermediateCert, 'utf-8'), + fs.readFile(caIntermediateFiles.privateKey, 'utf-8'), + fs.readFile(caIntermediateFiles.cert, 'utf-8'), + fs.readFile(caRootFiles.cert, 'utf-8') ]) - console.log({ - intermediatePrivateKey, - intermediateCert - }) - const deviceCert = await new Promise((resolve, reject) => createCertificate({ commonName: deviceId, serial: Math.floor(Math.random() * 1000000000), days: 365, - selfSigned: true, - config: [ - '[req]', - 'req_extensions = v3_req', - 'distinguished_name = req_distinguished_name', - '[req_distinguished_name]', - 'commonName = ' + deviceId, - '[v3_req]', - 'extendedKeyUsage = critical,clientAuth' - ].join('\n'), + config: leafCertConfig(deviceId), serviceKey: intermediatePrivateKey, serviceCertificate: intermediateCert }, (err, cert) => { @@ -70,7 +56,7 @@ export const generateDeviceCertificate = async ({ const certWithChain = (await Promise.all([ deviceCert.certificate, intermediateCert, - fs.readFile(caFiles.rootCert, 'utf-8'), + rootCert, ])).join(os.EOL) await Promise.all([ diff --git a/cli/iot/generateProofOfPosession.ts b/cli/iot/generateProofOfPosession.ts index 960e48d..713716b 100644 --- a/cli/iot/generateProofOfPosession.ts +++ b/cli/iot/generateProofOfPosession.ts @@ -1,6 +1,7 @@ import { promises as fs } from 'fs' -import { caFileLocations } from './caFileLocations' +import { CARootFileLocations } from './caFileLocations' import { createCertificate, CertificateCreationResult } from 'pem' +import { leafCertConfig } from './pemConfig' /** * Verifies the CA posessions @@ -14,29 +15,21 @@ export const generateProofOfPosession = async (args: { debug: (...message: any[]) => void }): Promise<{ verification: CertificateCreationResult, }> => { const { certsDir, log, debug, verificationCode } = args - const caFiles = caFileLocations(certsDir) + const caFiles = CARootFileLocations(certsDir) const [ rootKey, rootCert ] = await Promise.all([ - fs.readFile(caFiles.rootPrivateKey, 'utf-8'), - fs.readFile(caFiles.rootCert, 'utf-8'), + fs.readFile(caFiles.privateKey, 'utf-8'), + fs.readFile(caFiles.cert, 'utf-8'), ]) const verificationCert = await new Promise((resolve, reject) => createCertificate({ commonName: verificationCode, serial: Math.floor(Math.random() * 1000000000), days: 1, - config: [ - '[req]', - 'req_extensions = v3_req', - 'distinguished_name = req_distinguished_name', - '[req_distinguished_name]', - 'commonName = ' + verificationCode, - '[v3_req]', - 'extendedKeyUsage = critical,clientAuth' - ].join('\n'), + config: leafCertConfig(verificationCode), serviceKey: rootKey, serviceCertificate: rootCert }, (err, cert) => { diff --git a/cli/iot/intermediateRegistry.ts b/cli/iot/intermediateRegistry.ts new file mode 100644 index 0000000..0f39b4f --- /dev/null +++ b/cli/iot/intermediateRegistry.ts @@ -0,0 +1,32 @@ +import { promises as fs } from 'fs' +import * as path from 'path' + +export const CAIntermediateRegistryLocation = (certsDir: string) => ({ + registry: path.resolve(certsDir, 'intermediate.json'), +}) + +export const add = async ({ certsDir, id }: { certsDir: string, id: string }) => { + const intermediateRegistry = CAIntermediateRegistryLocation(certsDir).registry + let registry = [] as string[] + + try { + registry = [ + id, + ...(await list({ certsDir })) + ] + } catch { + registry = [id] + } finally { + await fs.writeFile(intermediateRegistry, JSON.stringify(registry), 'utf-8') + } +} + +export const list = async ({ certsDir }: { certsDir: string }): Promise => { + const intermediateRegistry = CAIntermediateRegistryLocation(certsDir).registry + try { + return JSON.parse(await fs.readFile(intermediateRegistry, 'utf-8')) + } catch { + return [] + } +} + diff --git a/cli/iot/pemConfig.ts b/cli/iot/pemConfig.ts new file mode 100644 index 0000000..daf3472 --- /dev/null +++ b/cli/iot/pemConfig.ts @@ -0,0 +1,20 @@ +export const caCertConfig = (commonName: string) => [ + '[req]', + 'req_extensions = v3_req', + 'distinguished_name = req_distinguished_name', + 'x509_extensions = v3_ca', + '[req_distinguished_name]', + 'commonName = ' + commonName, + '[v3_req]', + 'basicConstraints = critical,CA:true' +].join('\n') + +export const leafCertConfig = (commonName: string) => [ + '[req]', + 'req_extensions = v3_req', + 'distinguished_name = req_distinguished_name', + '[req_distinguished_name]', + 'commonName = ' + commonName, + '[v3_req]', + 'extendedKeyUsage = critical,clientAuth' +].join('\n') \ No newline at end of file diff --git a/cli/logging.ts b/cli/logging.ts new file mode 100644 index 0000000..0fcb332 --- /dev/null +++ b/cli/logging.ts @@ -0,0 +1,9 @@ +import chalk from 'chalk' + +export const log = (...message: any[]) => { + console.log(...message.map(m => chalk.magenta(m))) +} + +export const debug = (...message: any[]) => { + console.log(...message.map(m => chalk.cyan(m))) +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 47a7643..e4aea7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -646,6 +646,15 @@ "@types/node": "*" } }, + "@types/uuid": { + "version": "3.4.6", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.6.tgz", + "integrity": "sha512-cCdlC/1kGEZdEglzOieLDYBxHsvEOIg7kp/2FYyVR9Pxakq+Qf/inL3RKQ+PA8gOlI/NnL+fXmQH12nwcGzsHw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@typescript-eslint/eslint-plugin": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.9.0.tgz", diff --git a/package.json b/package.json index cfc5983..cf16cef 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "azure-iot-security-x509": "^1.6.0", "azure-iothub": "^1.11.0", "deepmerge": "^4.1.1", - "mqtt": "^3.0.0" + "mqtt": "^3.0.0", + "uuid": "^3.3.3" }, "devDependencies": { "@azure/arm-deviceprovisioningservices": "^2.1.0", @@ -46,6 +47,7 @@ "@types/jsonwebtoken": "^8.3.4", "@types/node": "^12.12.14", "@types/pem": "^1.9.5", + "@types/uuid": "^3.4.6", "check-node-version": "^4.0.1", "husky": "^3.0.9", "pem": "^1.14.3",