Skip to content
This repository has been archived by the owner on Feb 17, 2021. It is now read-only.

Commit

Permalink
feat(iot): register CA and verify
Browse files Browse the repository at this point in the history
  • Loading branch information
coderbyheart committed Nov 22, 2019
1 parent db3a495 commit f8c239d
Show file tree
Hide file tree
Showing 14 changed files with 5,091 additions and 1 deletion.
74 changes: 74 additions & 0 deletions cli/bifravst.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as program from 'commander'
import chalk from 'chalk'
import * as path from 'path'
import { registerCaCommand } from './commands/register-ca'
import { IotHubClient } from "@azure/arm-iothub";
import { AzureCliCredentials } from "@azure/ms-rest-nodeauth";
import { proofPosession } from './commands/proofPosession';

const bifravstCLI = async () => {
const certsDir = path.resolve(process.cwd(), 'certificates')

const creds = await AzureCliCredentials.create();

const { tokenInfo: { subscription } } = creds

console.log(chalk.yellow('Subscription ID:'), chalk.green(subscription))


const iotClient = new IotHubClient(creds, subscription);

program.description('Bifravst Command Line Interface')

const commands = [
registerCaCommand({
certsDir,
iotClient,
}),
proofPosession({
certsDir,
iotClient
})
]

let ran = false
commands.forEach(({ command, action, help, options }) => {
const cmd = program.command(command)
cmd
.action(async (...args) => {
try {
ran = true
await action(...args)
} catch (error) {
console.error(
chalk.red.inverse(' ERROR '),
chalk.red(`${command} failed!`),
)
console.error(chalk.red.inverse(' ERROR '), chalk.red(error))
process.exit(1)
}
})
.on('--help', () => {
console.log('')
console.log(chalk.yellow(help))
console.log('')
})
if (options) {
options.forEach(({ flags, description, defaultValue }) =>
cmd.option(flags, description, defaultValue),
)
}
})

program.parse(process.argv)

if (!ran) {
program.outputHelp(chalk.yellow)
throw new Error('No command selected!')
}
}

bifravstCLI().catch(err => {
console.error(chalk.red(err))
process.exit(1)
})
6 changes: 6 additions & 0 deletions cli/commands/CommandDefinition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type ComandDefinition = {
command: string
action: (...args: any) => Promise<void>
options?: { flags: string; description?: string; defaultValue?: any }[]
help: string
}
44 changes: 44 additions & 0 deletions cli/commands/proofPosession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import chalk from 'chalk'
import { ComandDefinition } from './CommandDefinition'
import { promises as fs } from 'fs'
import { IotHubClient } from "@azure/arm-iothub";
import { caFileLocations } from '../iot/caFileLocations';

export const proofPosession = ({
certsDir,
iotClient,
}: {
certsDir: string
iotClient: IotHubClient
}): ComandDefinition => ({
command: 'proof-ca-posession',
action: async () => {

const resourceGroupName = 'bifravst'
const iotHubName = 'bifravst'
const certificateName = 'bifravst-devices'

const cert = await iotClient.certificates.get(
resourceGroupName,
iotHubName,
certificateName,
)

const caFiles = caFileLocations(certsDir)
const verificationPEM = await fs.readFile(caFiles.verificationPEM, 'utf-8')

await iotClient.certificates.verify(
resourceGroupName,
iotHubName,
certificateName,
cert.etag as string,
{
certificate: verificationPEM
})

console.log(chalk.magenta(`Verified CA certificate on IoT Hub`, chalk.blueBright(iotHubName)))

console.log(chalk.green('You can now generate device certificates.'))
},
help: 'Creates a CA for devices and adds it to the registry',
})
65 changes: 65 additions & 0 deletions cli/commands/register-ca.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import chalk from 'chalk'
import { ComandDefinition } from './CommandDefinition'
import { generateCA } from '../iot/generateCA'
import { IotHubClient } from "@azure/arm-iothub";
import { generateProofOfPosession } from '../iot/generateProofOfPosession';

export const registerCaCommand = ({
certsDir,
iotClient,
}: {
certsDir: string
iotClient: IotHubClient
}): 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 { certificate } = await generateCA({
certsDir,
log,
debug
})
console.log(chalk.magenta(`CA certificate generated.`))

console.log(certificate)

const resourceGroupName = 'bifravst'
const iotHubName = 'bifravst'
const certificateName = `${iotHubName}-devices`

const iotHubCert = await iotClient.certificates.createOrUpdate(
resourceGroupName,
iotHubName,
certificateName,
{
certificate,
}
)

const cert = await iotClient.certificates.generateVerificationCode(
resourceGroupName,
iotHubName,
certificateName,
iotHubCert.etag as string,
)

await generateProofOfPosession({
certsDir,
log,
debug,
verificationCode: cert.properties?.verificationCode ?? ''
})

console.log(chalk.magenta(`Added CA certificate to registry`), chalk.blueBright(iotHubName))
console.log(chalk.green('You can now proof the posession using'), chalk.yellow(`node cli proof-ca-posession`))
},
help: 'Creates a CA for devices and adds it to the registry',
})
4 changes: 4 additions & 0 deletions cli/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env node

// eslint-disable-next-line
require('../dist/bifravst')
9 changes: 9 additions & 0 deletions cli/iot/caFileLocations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as path from 'path'

export const caFileLocations = (certsDir: string) => ({
cert: path.resolve(certsDir, 'CA.pem'),
privateKey: path.resolve(certsDir, 'CA.key'),
verificationKey: path.resolve(certsDir, 'CA.verification.key'),
verificationCSR: path.resolve(certsDir, 'CA.verification.csr'),
verificationPEM: path.resolve(certsDir, 'CA.verification.pem'),
})
14 changes: 14 additions & 0 deletions cli/iot/deviceFileLocations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as path from 'path'

export const deviceFileLocations = ({
certsDir,
deviceId,
}: {
certsDir: string
deviceId: string
}) => ({
privateKey: path.resolve(certsDir, `device-${deviceId}.private.pem`),
publicKey: path.resolve(certsDir, `device-${deviceId}.pem`),
csr: path.resolve(certsDir, `device-${deviceId}.csr`),
json: path.resolve(certsDir, `device-${deviceId}.json`)
})
4 changes: 4 additions & 0 deletions cli/iot/deviceTopics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const deviceTopics = (deviceId: string) => ({
config: `/devices/${deviceId}/config`,
state: `/devices/${deviceId}/state`,
})
68 changes: 68 additions & 0 deletions cli/iot/generateCA.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { promises as fs } from 'fs'
import { caFileLocations } from './caFileLocations'
import { run } from '../process/run'

/**
* Generates a CA certificate
*/
export const generateCA = async (args: {
certsDir: string
log: (...message: any[]) => void
debug: (...message: any[]) => void
}): Promise<{ certificate: string }> => {
const { certsDir, log, debug } = args
const caFiles = caFileLocations(certsDir)
try {
await fs.stat(certsDir)
} catch {
await fs.mkdir(certsDir)
log(`Created ${certsDir}`)
}

let certExists = false
try {
await fs.stat(caFiles.cert)
certExists = true
} catch {
// pass
}
if (certExists) {
throw new Error(`CA Certificate exists: ${caFiles.cert}!`)
}

// Now generate the CA

await run({
command: 'openssl',
args: ['genpkey', '-algorithm', 'RSA', '-out', caFiles.privateKey, '-pkeyopt', 'rsa_keygen_bits:2048'],
log: debug,
})

await run({
command: 'openssl',
args: [
'req',
'-x509',
'-new',
'-nodes',
'-key',
caFiles.privateKey,
'-sha256',
'-days',
'365',
'-out',
caFiles.cert,
'-subj',
'/CN=unused',
],
log: debug,
})

log(`Created CA certificate in ${caFiles.cert}`)

const certificate = await fs.readFile(caFiles.cert, 'utf-8')

return {
certificate
}
}
76 changes: 76 additions & 0 deletions cli/iot/generateDeviceCertificate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { promises as fs } from 'fs'
import { caFileLocations } from './caFileLocations'
import { deviceFileLocations } from './deviceFileLocations'
import { run } from '../process/run'

/**
* Generates a certificate for a device, signed with the CA
* @see https://docs.aws.amazon.com/iot/latest/developerguide/device-certs-your-own.html
*/
export const generateDeviceCertificate = async ({
certsDir,
log,
debug,
deviceId,
}: {
certsDir: string
deviceId: string
log?: (...message: any[]) => void
debug?: (...message: any[]) => void
}): Promise<{ deviceId: string, expires: Date }> => {
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 deviceFiles = deviceFileLocations({
certsDir,
deviceId,
})

// Create a device private key
await run({
command: 'openssl',
args: [
'genpkey', '-algorithm', 'RSA', '-out', deviceFiles.privateKey, '-pkeyopt', 'rsa_keygen_bits:2048'
],
log: debug,
})

// Create a CSR from the device private key
await run({
command: 'openssl',
args: [
'req', '-new', '-sha256', '-key', deviceFiles.privateKey, '-out', deviceFiles.csr, '-subj', '/CN=unused-device'
],
log: debug,
})

// Create a public key and sign it with the CA private key.
const validityInDays = 10950
await run({
command: 'openssl',
args: [
'x509',
'-req',
'-in',
deviceFiles.csr,
'-CAkey',
caFiles.privateKey,
'-CA',
caFiles.cert,
'-CAcreateserial',
'-days',
`${validityInDays}`,
'-sha256',
'-out',
deviceFiles.publicKey,
],
log: debug,
})

return { deviceId, expires: new Date(Date.now() + (validityInDays * 24 * 60 * 60 * 1000)) }
}
Loading

0 comments on commit f8c239d

Please sign in to comment.