Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions docs/fleet_provisioning/generate_claim_tpm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
#!/bin/bash
set -e

# Variables - modify these as needed
REGION="${AWS_DEFAULT_REGION:-us-west-2}"
ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
STACK_NAME="GreengrassFleetProvisioning"
CSR_COMMON_NAME="${CSR_COMMON_NAME:-aws-greengrass-nucleus-lite}"
TPM_KEY_HANDLE="${TPM_KEY_HANDLE:-0x81000002}"

# Calculate directories relative to script location
SCRIPT_DIR="$(pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../" && pwd)"
TEMP_DIR="${PROJECT_ROOT}/TPMFleetCerts"

# Check if fleet-provisioning-cfn.yaml exists
if [ ! -f "${SCRIPT_DIR}/fleet-provisioning-cfn.yaml" ]; then
exit 1
fi

# Check if TPM tools are available
if ! command -v tpm2_createprimary &> /dev/null || ! command -v tpm2_create &> /dev/null; then
echo "Error: TPM2 tools not found. Please install tpm2-tools package."
exit 1
fi

# Check if TPM key handle is already in use
if tpm2_getcap handles-persistent | grep -q "${TPM_KEY_HANDLE}"; then
echo "Warning: TPM key handle ${TPM_KEY_HANDLE} is already in use. Please change to the available key handle."
exit 1
fi

# Create certificate directory
mkdir -p "${TEMP_DIR}"

echo "=== Setting up AWS IoT Fleet Provisioning for Greengrass with TPM ==="
echo "Region: ${REGION}"
echo "Account ID: ${ACCOUNT_ID}"
echo "Stack Name: ${STACK_NAME}"
echo "TPM Key Handle: ${TPM_KEY_HANDLE}"
echo "Temporary Directory: ${TEMP_DIR}"

# Deploy CloudFormation stack
echo -e "\n=== Deploying CloudFormation stack ==="
STACK_STATUS=$(aws cloudformation describe-stacks --stack-name ${STACK_NAME} --region "${REGION}" --query "Stacks[0].StackStatus" --output text 2>/dev/null || echo "DOES_NOT_EXIST")

if [ "$STACK_STATUS" == "ROLLBACK_COMPLETE" ] || [ "$STACK_STATUS" == "CREATE_FAILED" ] || [ "$STACK_STATUS" == "UPDATE_FAILED" ] || [ "$STACK_STATUS" == "UPDATE_ROLLBACK_COMPLETE" ]; then
echo "Stack is in ${STACK_STATUS} state. Deleting it first..."
aws cloudformation delete-stack --stack-name ${STACK_NAME} --region "${REGION}"
echo "Waiting for stack deletion to complete..."
aws cloudformation wait stack-delete-complete --stack-name ${STACK_NAME} --region "${REGION}"
STACK_STATUS="DOES_NOT_EXIST"
fi

if [ "$STACK_STATUS" == "DOES_NOT_EXIST" ]; then
echo "Creating new CloudFormation stack: ${STACK_NAME}"
aws cloudformation create-stack \
--stack-name ${STACK_NAME} \
--template-body file://"${SCRIPT_DIR}"/fleet-provisioning-cfn.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--region "${REGION}"
echo "Waiting for stack creation to complete..."
aws cloudformation wait stack-create-complete --stack-name ${STACK_NAME} --region "${REGION}"
else
echo "Updating existing CloudFormation stack: ${STACK_NAME}"
aws cloudformation update-stack \
--stack-name ${STACK_NAME} \
--template-body file://"${SCRIPT_DIR}"/fleet-provisioning-cfn.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--region "${REGION}" || echo "No updates are to be performed."
fi

# Get outputs from CloudFormation stack
echo -e "\n=== Getting CloudFormation stack outputs ==="
PROVISIONING_TEMPLATE_NAME=$(aws cloudformation describe-stacks --stack-name ${STACK_NAME} --query "Stacks[0].Outputs[?OutputKey=='ProvisioningTemplateName'].OutputValue" --output text --region "${REGION}")
TOKEN_EXCHANGE_ROLE_ALIAS=$(aws cloudformation describe-stacks --stack-name ${STACK_NAME} --query "Stacks[0].Outputs[?OutputKey=='TokenExchangeRoleAlias'].OutputValue" --output text --region "${REGION}")
THING_GROUP_NAME=$(aws cloudformation describe-stacks --stack-name ${STACK_NAME} --query "Stacks[0].Outputs[?OutputKey=='ThingGroupName'].OutputValue" --output text --region "${REGION}")
MAC_VALIDATION_LAMBDA_ARN=$(aws cloudformation describe-stacks --stack-name ${STACK_NAME} --query "Stacks[0].Outputs[?OutputKey=='MacValidationLambdaArn'].OutputValue" --output text --region "${REGION}")

echo "Provisioning Template Name: ${PROVISIONING_TEMPLATE_NAME}"
echo "Token Exchange Role Alias: ${TOKEN_EXCHANGE_ROLE_ALIAS}"
echo "Thing Group Name: ${THING_GROUP_NAME}"
echo "MAC Validation Lambda ARN: ${MAC_VALIDATION_LAMBDA_ARN}"

# Generate TPM claim key and CSR
echo -e "\n=== Generating TPM claim key and CSR ==="
echo "Creating TPM primary key..."
tpm2_createprimary -C o -c "${TEMP_DIR}/primary.ctx"

echo "Creating ECC key..."
tpm2_create -C "${TEMP_DIR}/primary.ctx" -g sha256 -G ecc256 -r "${TEMP_DIR}/device.priv" -u "${TEMP_DIR}/device.pub"

echo "Loading the key..."
tpm2_load -C "${TEMP_DIR}/primary.ctx" -r "${TEMP_DIR}/device.priv" -u "${TEMP_DIR}/device.pub" -c "${TEMP_DIR}/device.ctx"

echo "Making the key persistent..."
tpm2_evictcontrol -C o -c "${TEMP_DIR}/device.ctx" ${TPM_KEY_HANDLE}

echo "Generating CSR with TPM claim key..."
openssl req -new -provider tpm2 -key "handle:${TPM_KEY_HANDLE}" \
-out "${TEMP_DIR}/claim.csr" \
-subj "/CN=TPM_CSR"
echo "Successfully generated the CSR"

# Create certificate from CSR
echo -e "\n=== Creating claim certificate from CSR ==="
echo "Creating certificate from CSR..."
aws iot create-certificate-from-csr \
--certificate-signing-request file://"${TEMP_DIR}/claim.csr" \
--set-as-active \
--region "${REGION}" > "${TEMP_DIR}"/cert-details.json
echo "Successfully created the certificate"

# Extract certificate from response and save to file
jq -r '.certificatePem' "${TEMP_DIR}"/cert-details.json > "${TEMP_DIR}/certificate.pem.crt"

# Attach the fleet provisioning policy to the claim certificate
echo "Attaching FleetProvisioningPolicy to certificate..."
CERT_ARN=$(jq -r '.certificateArn' "${TEMP_DIR}"/cert-details.json)
CERT_ID=$(jq -r '.certificateId' "${TEMP_DIR}"/cert-details.json)
aws iot attach-policy \
--policy-name "FleetProvisioningPolicy-${STACK_NAME}" \
--target "${CERT_ARN}" \
--region "${REGION}"
echo "Successfully attached the policy to the certificate"

# Download the Amazon root CA certificate
echo -e "\n=== Downloading Amazon root CA certificate ==="
curl -s -o "${TEMP_DIR}"/AmazonRootCA1.pem https://www.amazontrust.com/repository/AmazonRootCA1.pem
echo "Successfully downloaded the Amazon root CA certificate"

# Get IoT endpoints
echo -e "\n=== Getting IoT endpoints ==="
IOT_DATA_ENDPOINT=$(aws iot describe-endpoint --endpoint-type iot:Data-ATS --region "${REGION}" --output text)
IOT_CRED_ENDPOINT=$(aws iot describe-endpoint --endpoint-type iot:CredentialProvider --region "${REGION}" --output text)

echo "IoT Data Endpoint: ${IOT_DATA_ENDPOINT}"
echo "IoT Credential Endpoint: ${IOT_CRED_ENDPOINT}"

# Create part.config.yaml snippet for TPM
echo -e "\n=== Creating part.config.yaml snippet ==="
{
printf '# Fleet provisioning configuration with TPM\n'
printf 'aws.greengrass.fleet_provisioning:\n'
printf '\tconfiguration:\n'
printf '\t\tiotDataEndpoint: "%s"\n' "${IOT_DATA_ENDPOINT}"
printf '\t\tiotCredEndpoint: "%s"\n' "${IOT_CRED_ENDPOINT}"
printf '\t\tclaimKeyPath: "handle:%s"\n' "${TPM_KEY_HANDLE}"
printf '\t\tclaimCertPath: "%s"\n' "${TEMP_DIR}/certificate.pem.crt"
printf '\t\trootCaPath: "%s"\n' "${TEMP_DIR}/AmazonRootCA1.pem"
printf '\t\ttemplateName: "%s"\n' "${PROVISIONING_TEMPLATE_NAME}"
printf '\t\tcsrCommonName: "%s"\n' "${CSR_COMMON_NAME}"
} > "${TEMP_DIR}"/part.config.yaml

echo -e "\n=== Fleet provisioning setup complete ==="
echo "Files generated in: ${TEMP_DIR}"
echo " - part.config.yaml (TPM-enabled)"
echo " - certificate.pem.crt"
echo " - AmazonRootCA1.pem"
echo " - claim.csr"
echo ""
echo "TPM Key Handle: ${TPM_KEY_HANDLE}"

# Display certificate ID if available
if [ -f "${TEMP_DIR}/cert-details.json" ]; then
CERT_ID=$(jq -r '.certificateId' "${TEMP_DIR}"/cert-details.json)
echo "Claim Certificate ID: ${CERT_ID}"
fi
175 changes: 175 additions & 0 deletions modules/fleet-provisioning/src/tpm_pki.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// aws-greengrass-lite - AWS IoT Greengrass runtime for constrained devices
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
#include "tpm_pki.h"
#include <ggl/log.h>
#include <openssl/crypto.h>
#include <tss2/tss2_esys.h>
#include <tss2/tss2_mu.h>
#include <tss2/tss2_rc.h>
#include <stdio.h>
#include <stdlib.h>

#define TPM_PERSISTENT_HANDLE 0x81000003

static ESYS_CONTEXT *esys_ctx = NULL;

GglError ggl_tpm_generate_keys(void) {
TSS2_RC rc;

rc = Esys_Initialize(&esys_ctx, NULL, NULL);
if (rc != TSS2_RC_SUCCESS) {
GGL_LOGE("Failed to initialize ESYS context: 0x%x", rc);
return GGL_ERR_FAILURE;
}

// Check if key already exists
ESYS_TR persistent_handle = ESYS_TR_NONE;
rc = Esys_TR_FromTPMPublic(esys_ctx, TPM_PERSISTENT_HANDLE,
ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE,
&persistent_handle);
if (rc == TSS2_RC_SUCCESS) {
GGL_LOGI("TPM key already exists at handle 0x%x", TPM_PERSISTENT_HANDLE);
Esys_TR_Close(esys_ctx, &persistent_handle);
Esys_Finalize(&esys_ctx);
return GGL_ERR_INVALID;
} else if ((rc & ~TPM2_RC_N_MASK) != TPM2_RC_HANDLE) {
GGL_LOGE("Failed to query TPM handle: 0x%x", rc);
Esys_Finalize(&esys_ctx);
return GGL_ERR_FAILURE;
}

// Use default ECC P-256 template for primary key
TPM2B_PUBLIC in_public_primary = {0};
in_public_primary.publicArea.type = TPM2_ALG_ECC;
in_public_primary.publicArea.nameAlg = TPM2_ALG_SHA256;
in_public_primary.publicArea.objectAttributes =
TPMA_OBJECT_RESTRICTED |
TPMA_OBJECT_DECRYPT |
TPMA_OBJECT_FIXEDTPM |
TPMA_OBJECT_FIXEDPARENT |
TPMA_OBJECT_SENSITIVEDATAORIGIN |
TPMA_OBJECT_USERWITHAUTH;
in_public_primary.publicArea.parameters.eccDetail.symmetric.algorithm = TPM2_ALG_AES;
in_public_primary.publicArea.parameters.eccDetail.symmetric.keyBits.aes = 128;
in_public_primary.publicArea.parameters.eccDetail.symmetric.mode.aes = TPM2_ALG_CFB;
in_public_primary.publicArea.parameters.eccDetail.scheme.scheme = TPM2_ALG_NULL;
in_public_primary.publicArea.parameters.eccDetail.curveID = TPM2_ECC_NIST_P256;
in_public_primary.publicArea.parameters.eccDetail.kdf.scheme = TPM2_ALG_NULL;
in_public_primary.publicArea.unique.ecc.x.size = 0;
in_public_primary.publicArea.unique.ecc.y.size = 0;

TPM2B_SENSITIVE_CREATE in_sensitive = {
.size = 0,
.sensitive = {
.userAuth = {.size = 0},
.data = {.size = 0}
}
};

TPM2B_DATA outside_info = {.size = 0};
TPML_PCR_SELECTION creation_pcr = {.count = 0};

// Create primary key
ESYS_TR primary_handle = ESYS_TR_NONE;
rc = Esys_CreatePrimary(esys_ctx, ESYS_TR_RH_OWNER,
ESYS_TR_PASSWORD, ESYS_TR_NONE, ESYS_TR_NONE,
&in_sensitive, &in_public_primary, &outside_info, &creation_pcr,
&primary_handle, NULL, NULL, NULL, NULL);
if (rc != TSS2_RC_SUCCESS) {
GGL_LOGE("Failed to create primary key: 0x%x", rc);
Esys_Finalize(&esys_ctx);
return GGL_ERR_FAILURE;
}

// Use default ECC P-256 template for child key
TPM2B_PUBLIC in_public_child = {0};
in_public_child.publicArea.type = TPM2_ALG_ECC;
in_public_child.publicArea.nameAlg = TPM2_ALG_SHA256;
in_public_child.publicArea.objectAttributes =
TPMA_OBJECT_SIGN_ENCRYPT |
TPMA_OBJECT_FIXEDTPM |
TPMA_OBJECT_FIXEDPARENT |
TPMA_OBJECT_SENSITIVEDATAORIGIN |
TPMA_OBJECT_USERWITHAUTH;
in_public_child.publicArea.parameters.eccDetail.symmetric.algorithm = TPM2_ALG_NULL;
in_public_child.publicArea.parameters.eccDetail.scheme.scheme = TPM2_ALG_ECDSA;
in_public_child.publicArea.parameters.eccDetail.scheme.details.ecdsa.hashAlg = TPM2_ALG_SHA256;
in_public_child.publicArea.parameters.eccDetail.curveID = TPM2_ECC_NIST_P256;
in_public_child.publicArea.parameters.eccDetail.kdf.scheme = TPM2_ALG_NULL;
in_public_child.publicArea.unique.ecc.x.size = 0;
in_public_child.publicArea.unique.ecc.y.size = 0;

// Create child ECC key
TPM2B_PRIVATE *out_private = NULL;
TPM2B_PUBLIC *out_public = NULL;
rc = Esys_Create(esys_ctx, primary_handle,
ESYS_TR_PASSWORD, ESYS_TR_NONE, ESYS_TR_NONE,
&in_sensitive, &in_public_child, &outside_info, &creation_pcr,
&out_private, &out_public, NULL, NULL, NULL);
if (rc != TSS2_RC_SUCCESS) {
GGL_LOGE("Failed to create child key: 0x%x", rc);
Esys_FlushContext(esys_ctx, primary_handle);
Esys_Finalize(&esys_ctx);
return GGL_ERR_FAILURE;
}

// Load child key
ESYS_TR child_handle = ESYS_TR_NONE;
rc = Esys_Load(esys_ctx, primary_handle,
ESYS_TR_PASSWORD, ESYS_TR_NONE, ESYS_TR_NONE,
out_private, out_public, &child_handle);
if (rc != TSS2_RC_SUCCESS) {
GGL_LOGE("Failed to load child key: 0x%x", rc);
Esys_FlushContext(esys_ctx, primary_handle);
Esys_Finalize(&esys_ctx);
return GGL_ERR_FAILURE;
}

// Make child key persistent
ESYS_TR persistent_out = ESYS_TR_NONE;
rc = Esys_EvictControl(esys_ctx, ESYS_TR_RH_OWNER, child_handle,
ESYS_TR_PASSWORD, ESYS_TR_NONE, ESYS_TR_NONE,
TPM_PERSISTENT_HANDLE, &persistent_out);
if (rc != TSS2_RC_SUCCESS) {
GGL_LOGE("Failed to make key persistent: 0x%x", rc);
Esys_FlushContext(esys_ctx, primary_handle);
Esys_FlushContext(esys_ctx, child_handle);
Esys_Finalize(&esys_ctx);
return GGL_ERR_FAILURE;
}

// Cleanup
GGL_LOGI("TPM key created and made persistent at handle 0x%x", TPM_PERSISTENT_HANDLE);
Esys_FlushContext(esys_ctx, primary_handle);
Esys_FlushContext(esys_ctx, child_handle);
if (persistent_out != ESYS_TR_NONE)
Esys_TR_Close(esys_ctx, &persistent_out);

free(out_private);
free(out_public);
Esys_Finalize(&esys_ctx);
return GGL_ERR_OK;
}

GglError ggl_tpm_generate_csr(GglBuffer csr_file_path) {

// Use OpenSSL command with TPM2 provider
static char cmd[512];
snprintf(cmd, sizeof(cmd),
"openssl req -new -provider tpm2 -key \"handle:0x%x\" "
"-out %.*s -subj \"/CN=TPMThing\"",
TPM_PERSISTENT_HANDLE,
(int)csr_file_path.len, (char*)csr_file_path.data);

GGL_LOGI("Generating CSR with command: %s", cmd);

int result = system(cmd);
if (result != 0) {
GGL_LOGE("Failed to generate CSR using OpenSSL command");
return GGL_ERR_FAILURE;
}

GGL_LOGI("CSR generated successfully using TPM key");
return GGL_ERR_OK;
}
14 changes: 14 additions & 0 deletions modules/fleet-provisioning/src/tpm_pki.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// aws-greengrass-lite - AWS IoT Greengrass runtime for constrained devices
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

#ifndef TPM_PKI_H
#define TPM_PKI_H

#include <ggl/buffer.h>
#include <ggl/error.h>

GglError ggl_tpm_generate_keys(void);
GglError ggl_tpm_generate_csr(GglBuffer csr_file_path);

#endif
Loading