This repository has been archived by the owner on Feb 17, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
db3a495
commit f8c239d
Showing
14 changed files
with
5,091 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'), | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) } | ||
} |
Oops, something went wrong.