Forked from mvisintin/configurationUpdate.js
Last active
April 13, 2022 12:35
Minimal configuration update example
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
// Note you must be authorised to use the provided GCloud project. | |
// Use `gcloud auth application-default login` to auth for the current machine | |
const iot = require('@google-cloud/iot'); | |
const { KeyManagementServiceClient } = require('@google-cloud/kms'); | |
const uuid = require('uuid'); | |
const { readFileSync } = require('fs'); | |
const crypto = require('crypto'); | |
const { config } = require('process'); | |
const iotClient = new iot.v1.DeviceManagerClient(); | |
const kmsClient = new KeyManagementServiceClient(); | |
const deviceId = 'clusterId-5b10fbfc-8e99-42a2-b3e6-cdc6f8e7bd71'; | |
const projectId = 'portal-dev-340714'; | |
const cloudRegion = 'europe-west1'; | |
const containerName = 'clusters'; | |
const licence = readFileSync('./licence.txt').toString(); | |
// RPC Path to IOT device | |
const devicePath = iotClient.devicePath( | |
projectId, | |
cloudRegion, | |
containerName, | |
deviceId | |
); | |
// RPC Path to key ring | |
const keyRingPath = kmsClient.keyRingPath( | |
projectId, | |
cloudRegion, | |
containerName | |
); | |
// RPC Path to key | |
const keyPath = kmsClient.cryptoKeyVersionPath( | |
projectId, | |
cloudRegion, | |
containerName, | |
deviceId, | |
1 | |
); | |
/** | |
* Creates an asymmetric key pair for signing. | |
* The corresponding public key will be delivered to the cluster with the | |
* configuration call | |
*/ | |
async function createKey() { | |
const [key] = await kmsClient.createCryptoKey({ | |
parent: keyRingPath, | |
cryptoKeyId: deviceId, | |
cryptoKey: { | |
purpose: 'ASYMMETRIC_SIGN', | |
versionTemplate: { | |
algorithm: 'RSA_SIGN_PKCS1_4096_SHA512', | |
}, | |
}, | |
}); | |
return key; | |
} | |
/** | |
* Creates a signature based on the configurationBody passed to the function | |
*/ | |
const signConfiguration = async (configurationBody) => { | |
const hash = crypto.createHash('sha512'); | |
hash.update(configurationBody); | |
const [signResponse] = await kmsClient.asymmetricSign({ | |
name: keyPath, | |
digest: { | |
sha512: hash.digest(), | |
}, | |
}); | |
return signResponse.signature.toString('base64'); | |
}; | |
/** | |
* Retrieves the last configuration applied to the device (up to 10 can be | |
* stored) | |
*/ | |
const getConfiguration = async () => { | |
const [response] = await iotClient.listDeviceConfigVersions({ | |
name: devicePath, | |
}); | |
const config = Buffer.from( | |
response.deviceConfigs[0].binaryData, | |
'base64' | |
).toString(); | |
return config || ''; | |
}; | |
/** | |
* Applies a new configuration against the device | |
*/ | |
async function applyNewConfig(configuration) { | |
const request = { | |
name: devicePath, | |
binaryData: Buffer.from(JSON.stringify(configuration)).toString('base64'), | |
}; | |
const [response] = await iotClient.modifyCloudToDeviceConfig(request); | |
console.log('applyNewConfig success:', response); | |
} | |
/** | |
* This is what the portal manager will do to update the configuration | |
* - Retrieves the last configuration applied to the device | |
* - Creates a new configurationId | |
* - Creates a signature based on the configuration.body | |
* - Merges previous configuration.body and new one | |
* - Applies the resulted configuration | |
*/ | |
const updateConfig = async () => { | |
const configurationId = uuid.v4(); | |
const body = { | |
licence: Buffer.from(licence).toString('base64'), | |
configurationId, | |
}; | |
let bodyJsonStr = JSON.stringify(body); | |
let bodyB64 = Buffer.from(bodyJsonStr).toString('base64'); | |
const configuration = { | |
body: bodyB64, | |
signature: await signConfiguration(`${bodyB64}`), | |
}; | |
return applyNewConfig(configuration); | |
}; | |
/** | |
* This part simulates what the portal-manager would do, main difference | |
* is that here I am retrieving the publicKey from Google KMS while the | |
* portal-manager would have the public key stored in a config map or something | |
* similar. | |
* | |
* Other difference is that the configuration will be received from the specific | |
* topic. | |
* | |
* Rest is similar: | |
* - Receives new configuration | |
* - Configuration is parsed as JSON | |
* - Verifies signature using the publicKey against configuration body | |
* - ... | |
*/ | |
const verifyConfiguration = async () => { | |
const configuration = JSON.parse(await getConfiguration()); | |
const verifier = crypto.createVerify('SHA512'); | |
verifier.update(configuration.body); | |
verifier.end(); | |
const publicKey = await kmsClient | |
.getPublicKey({ name: keyPath }) | |
.then((res) => res[0].pem); | |
console.log('verifyConfig - retrieved config: ', configuration); | |
console.log( | |
'verifyConfig - publicKey: ', | |
Buffer.from(publicKey).toString('base64') | |
); | |
console.log( | |
'verifyConfig - config is valid?', | |
verifier.verify(publicKey, configuration.signature, 'base64') | |
); | |
}; | |
async function run() { | |
// await createKey(); | |
await updateConfig(); | |
await verifyConfiguration(); | |
} | |
run(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment