aws, iot, sécurité, serverless

Gestion des certificats IoT en mode auto-service sans serveur - Partie 1

2024-11-27
This post cover image
aws cloud serverless sécurité iot

Ce fichier a ete traduit automatiquement par IA, des erreurs peuvent survenir

J'ai travaillé sur plusieurs solutions IoT différentes au fil des années. Une chose qu'elles ont toutes en commun est le besoin de certificats de confiance qui peuvent être utilisés à la fois pour établir la connexion, mais aussi les identités des appareils. Les appareils et les serveurs doivent se faire confiance pour établir une communication sécurisée, garantir l'intégrité des données et prévenir les attaques malveillantes. Une partie importante de cette confiance est l'Infrastructure à Clés Publiques (PKI), où les certificats et les Autorités de Certification (AC) jouent des rôles vitaux, il est également nécessaire de gérer une grande quantité de certificats de manière simple.

Dans certains projets, nous avons développé notre propre solution PKI interne, ce qui entraîne une complexité et des exigences de sécurité. Dans d'autres, nous avons utilisé des solutions SaaS, comme DigiCert IoT Trust Manager, AWS IoT Core et Amazon Private CA.

Cependant, lorsqu'il s'agit de développement, et parfois même d'environnements de test, il n'était pas rare que nous utilisions des certificats auto-signés, avec un portail auto-service simple pour créer différents certificats.

Dans cet article, qui est la première partie d'une série de deux, nous allons présenter quelques bases autour des certificats et du PKI. Nous allons également commencer à créer les bases d'une API sans serveur qui peut être utilisée pour créer un portail auto-service pour la génération de certificats.

Pourquoi construire une API auto-service ?

Comme dit, dans un système IoT, des milliers d'appareils doivent interagir avec des serveurs, des courtiers IoT et entre eux. Chacune de ces interactions doit être sécurisée, ce qui signifie :

  • Les serveurs doivent authentifier les appareils pour s'assurer qu'ils sont légitimes.
  • Les appareils doivent authentifier les serveurs pour s'assurer qu'ils se connectent à une source de confiance.

Les certificats sont donc délivrés aux serveurs et aux appareils, créant une chaîne de confiance ancrée à une AC racine.

Une API auto-service pour la gestion des certificats permet :

  • L'automatisation : Les appareils et les serveurs peuvent demander et renouveler les certificats par programme.
  • La scalabilité : Au fur et à mesure que notre environnement IoT se développe, l'API peut gérer la demande croissante de certificats.
  • L'apprentissage et les tests : Avant d'adopter un service géré, la construction de votre propre système de certificats vous aide à comprendre le fonctionnement du PKI.

Aperçu des certificats, des AC et de la confiance

Qu'est-ce qu'une Autorité de Certification (AC) ?

Une AC est une entité de confiance responsable de la délivrance des certificats numériques. Ces certificats lient une clé publique à une identité (par exemple, un serveur, un appareil ou un utilisateur), permettant la confiance entre les entités.

Les AC sont structurées dans une hiérarchie :

AC racine

  • L'ancre de confiance ultime.
  • Auto-signée et hautement sécurisée.
  • Ne doit jamais être utilisée pour délivrer directement des certificats d'entités finales.

AC intermédiaire

  • Livrée et signée par l'AC racine.
  • Délègue la responsabilité de la délivrance des certificats aux entités finales (par exemple, les appareils et les serveurs).
  • Limite l'exposition de l'AC racine.

Certificats d'entités finales, feuilles

  • Certificats pour les serveurs, les appareils IoT ou les utilisateurs.
  • Livrés par une AC intermédiaire et utilisés dans la communication client-serveur ou l'authentification mutuelle.

Chaînes de certificats et confiance

Une chaîne de certificats est une séquence de certificats, où chaque certificat de la chaîne est signé par le certificat suivant. Elle représente la relation hiérarchique entre un certificat et son émetteur.

Image montrant une chaîne de certificats

Une chaîne peut consister en un ou plusieurs certificats intermédiaires. Dans l'image ci-dessus, la chaîne est illustrée avec deux AC intermédiaires.

Pendant une négociation TLS

Simplifiée, la négociation serait :

  • Le serveur présente son certificat au client.
  • Le client valide le certificat du serveur en retraçant la chaîne jusqu'à une AC racine de confiance dans son magasin de certificats. Lorsque vous utilisez une AC racine auto-signée, vous devez vous assurer que le paquet est présent dans le magasin de confiance ou inclus dans la tentative de connexion.

Dans un scénario où l'authentification mutuelle est requise, le serveur effectue le même processus pour le certificat du client.

Confiance à travers plusieurs AC intermédiaires

Dans une configuration IoT, il est courant d'avoir une AC intermédiaire délivrant des certificats de serveur (par exemple, pour les courtiers IoT), et une autre AC intermédiaire délivrant des certificats de client (par exemple, pour les appareils). Pour qu'un certificat de client (signé par une AC intermédiaire) fasse confiance à un certificat de serveur (signé par une autre AC intermédiaire), les deux doivent :

Faire partie de la même hiérarchie de confiance.

  • Les deux AC intermédiaires doivent être signées par la même AC racine.
  • L'AC racine est l'ancre de confiance commune.

Être validés contre la chaîne.

  • Le client vérifie la chaîne de certificats du serveur, la retraçant jusqu'à l'AC racine.
  • Le serveur vérifie la chaîne de certificats du client de manière similaire.

Cette configuration assure la scalabilité et la séparation des responsabilités. L'AC intermédiaire des serveurs se concentre sur les serveurs et les courtiers. L'AC intermédiaire des appareils se concentre sur les appareils IoT.

Mise en œuvre

Maintenant, commençons à mettre en œuvre cette API auto-service. Nous allons la construire entièrement sans serveur en utilisant Amazon API Gateway et des fonctions Lambda, avec le stockage des certificats dans S3 et Certificate Manager. Cette première partie de la série de blogs ne créera que la première partie très basique de l'API, que nous étendrons dans la deuxième partie. Vous pouvez trouver tout le code source sur Serverless-Handbook Self Service IoT Certificate management

Image montrant un aperçu de l'API

Nous allons configurer un API Gateway avec trois points de terminaison pour créer notre AC racine, notre AC intermédiaire et notre certificat de serveur. Tout sera stocké dans un bucket S3, et le certificat de serveur sera également importé dans Certificate Manager.

API REST

Voici d'abord un aperçu de l'API REST et de sa structure.

Point de terminaisonMéthodeDescription
/certificates/rootPOSTCréer une nouvelle AC racine.
/certificates/intermediatePOSTCréer une nouvelle AC intermédiaire.
/certificates/serverPOSTCréer un nouveau certificat de serveur.

J'ai décidé d'utiliser des chemins séparés (par exemple, /certificates/root, /certificates/intermediate, /certificates/server) plutôt qu'un seul point de terminaison avec un paramètre de type (par exemple, /certificates avec type en entrée) car je pense que cela s'aligne mieux avec les principes REST et améliore la lisibilité et l'utilisabilité de l'API.

Il est plus facile d'étendre l'API avec un nouveau type de certificat (par exemple, appareil), car nous pouvons créer un nouveau chemin comme /certificates/device sans impacter les chemins existants. Les chemins séparés segmentent naturellement les ressources, réduisant le besoin de filtrage côté client. Je pense que récupérer toutes les AC racines est plus simple avec GET /certificates/root que GET /certificates?type=root.

Infrastructure commune

Tout d'abord, déployons l'infrastructure commune, dans ce cas, il s'agit simplement du bucket S3, nous ajouterons plus de choses dans ce modèle plus tard.

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: Infrastructure commune pour la gestion des certificats auto-service
Parameters:
  ApplicationName:
    Type: String
    Description: Nom de l'application propriétaire
    Default: image-moderation

Resources:
  StorageBucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      BucketName: !Sub ${ApplicationName}-storage-bucket

Outputs:
  StorageBucketName:
    Description: Le nom du bucket de certificats
    Value: !Ref StorageBucket
    Export:
      Name: !Sub ${AWS::StackName}:certificate-bucket-name

Ensuite, nous pouvons configurer les points de terminaison en utilisant SAM et AWS::Serverless::Api et les trois fonctions Lambda qui soutiennent l'API.

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: Créer l'API pour la gestion des certificats auto-service
Parameters:
  ApplicationName:
    Type: String
    Description: Nom de l'application propriétaire
  CommonInfraStackName:
    Type: String
    Description: Le nom de la pile commune qui contient le bus EventBridge et plus encore

Globals:
  Function:
    Timeout: 30
    MemorySize: 2048
    Runtime: python3.12
    Environment:
        Variables:
          CERTIFICATE_BUCKET_NAME: 
            Fn::ImportValue: !Sub "${CommonInfraStackName}:certificate-bucket-name"

Resources:
  LambdaGenerateRootCA:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: Lambda/API/GenerateRootCA
      Handler: handler.handler
      Policies:
        - S3FullAccessPolicy:
            BucketName: 
              Fn::ImportValue: !Sub "${CommonInfraStackName}:certificate-bucket-name"
      Events:
        CreateRootCAApi:
          Type: Api
          Properties:
            Path: /certificates/root
            Method: post
            RestApiId: !Ref GenerateCertificatesApi

  LambdaGenerateIntermediateCA:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: Lambda/API/GenerateIntermediateCA
      Handler: handler.handler
      Policies:
        - S3FullAccessPolicy:
            BucketName: 
              Fn::ImportValue: !Sub "${CommonInfraStackName}:certificate-bucket-name"
      Events:
        CreateIntermediateCAApi:
          Type: Api
          Properties:
            Path: /certificates/intermediate
            Method: post
            RestApiId: !Ref GenerateCertificatesApi   

  LambdaGenerateDeviceCertificate:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: Lambda/API/GenerateDeviceCert
      Handler: handler.handler
      Policies:
        - S3FullAccessPolicy:
            BucketName: 
              Fn::ImportValue: !Sub "${CommonInfraStackName}:certificate-bucket-name"      

  LambdaGenerateServerCertificate:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: Lambda/API/GenerateServerCert
      Handler: handler.handler
      Policies:
        - S3FullAccessPolicy:
            BucketName: 
              Fn::ImportValue: !Sub "${CommonInfraStackName}:certificate-bucket-name"
        - Version: "2012-10-17"
          Statement:
            Action:
              - acm:*
            Effect: Allow
            Resource: "*"
      Events:
        CreateServerCAApi:
          Type: Api
          Properties:
            Path: /certificates/server
            Method: post
            RestApiId: !Ref GenerateCertificatesApi

  GenerateCertificatesApi:
    Type: AWS::Serverless::Api
    Properties:
      Description: API pour créer et gérer les certificats
      Name: !Sub ${ApplicationName}-api
      StageName: prod
      OpenApiVersion: '3.0.1'
      AlwaysDeploy: true
      EndpointConfiguration: REGIONAL

Nos fonctions Lambda sont en Python et nous utilisons la bibliothèque cryptography pour créer les certificats, voici l'implémentation pour créer un certificat de serveur. Pour une implémentation complète, visitez Serverless-Handbook Self Service IoT Certificate management.

import boto3
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization, hashes
import datetime
import os
import json

s3_client = boto3.client("s3")
acm_client = boto3.client("acm")


def create_server_certificate(
    intermediate_private_key_pem, 
    intermediate_cert_pem, 
    fqdn,
    country,
    state,
    organization,
    validity_days,
):
    # Charger la clé privée et le certificat de l'AC intermédiaire
    intermediate_private_key = serialization.load_pem_private_key(
        intermediate_private_key_pem, password=None
    )
    intermediate_cert = x509.load_pem_x509_certificate(intermediate_cert_pem)

    # Générer une clé privée pour le certificat de serveur
    server_private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    server_private_key_pem = server_private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption(),
    )

    subject = x509.Name(
        [
            x509.NameAttribute(NameOID.COUNTRY_NAME, country),
            x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, state),
            x509.NameAttribute(NameOID.ORGANIZATION_NAME, organization),
            x509.NameAttribute(NameOID.COMMON_NAME, fqdn),
        ]
    )
    server_certificate = (
        x509.CertificateBuilder()
        .subject_name(subject)
        .issuer_name(intermediate_cert.subject)  # Signé par l'AC intermédiaire
        .public_key(server_private_key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(datetime.datetime.now(datetime.timezone.utc))
        .not_valid_after(
            datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=validity_days)
        )
        .add_extension(
            x509.SubjectAlternativeName([x509.DNSName(fqdn)]),
            critical=False,
        )
        .add_extension(
            x509.BasicConstraints(ca=False, path_length=None),
            critical=True,
        )
        .add_extension(
            x509.KeyUsage(
                digital_signature=True,
                key_encipherment=True,
                key_cert_sign=False,
                crl_sign=False,
                content_commitment=False,
                data_encipherment=False,
                encipher_only=False,
                decipher_only=False,
                key_agreement=False,
            ),
            critical=True,
        )
        .sign(intermediate_private_key, hashes.SHA256())
    )
    server_cert_pem = server_certificate.public_bytes(serialization.Encoding.PEM)

    return server_private_key_pem, server_cert_pem


def handler(event, context):
    
    body = json.loads(event["body"])

    bucket_name = os.environ.get("CERTIFICATE_BUCKET_NAME")

    # Récupérer la clé privée et le certificat de l'AC intermédiaire depuis S3
    intermediate_private_key = s3_client.get_object(
        Bucket=bucket_name, Key="intermediate_ca/private_key.pem"
    )["Body"].read()

    intermediate_cert = s3_client.get_object(
        Bucket=bucket_name, Key="intermediate_ca/certificate.pem"
    )["Body"].read()

    # Récupérer la chaîne de certificats depuis S3
    cert_chain_pem = s3_client.get_object(
        Bucket=bucket_name, Key="intermediate_ca/certificate_chain.pem"
    )["Body"].read()

    # Créer le certificat de serveur
    server_private_key_pem, server_cert_pem = create_server_certificate(
        intermediate_private_key,
        intermediate_cert,
        body["fqdn"],
        body["country"],
        body["state"],
        body["organization"],
        body["validity_days"],
    )

    s3_folder = f"server_certificates/{body["fqdn"]}/"

    # Télécharger le certificat de serveur et la clé privée sur S3
    s3_client.put_object(
        Bucket=bucket_name,
        Key=f"{s3_folder}private_key.pem",
        Body=server_private_key_pem,
    )
    s3_client.put_object(
        Bucket=bucket_name, Key=f"{s3_folder}certificate.pem", Body=server_cert_pem
    )

    # Importer le certificat dans ACM
    response = acm_client.import_certificate(
        Certificate=server_cert_pem,
        PrivateKey=server_private_key_pem,
        CertificateChain=cert_chain_pem,
    )

    return {
        "statusCode": 200,
        "body": json.dumps(
            {
                "message": "Le certificat de serveur a été créé, téléchargé sur S3 et importé dans ACM.",
                "fqdn": body["fqdn"],
                "s3_folder": s3_folder,
                "acm_certificate_arn": response["CertificateArn"],
            }
        ),
    }

Conclusion

Cet article a couvert tout, des chaînes de certificats à la validation de la confiance dans les systèmes IoT. La construction de votre propre API auto-service est un bon moyen d'apprendre sur les certificats et le PKI. En production, cependant, optez toujours pour des solutions gérées qui offrent automatisation, conformité et scalabilité.

Les certificats sont le fondement de la sécurité IoT
Les certificats et les AC assurent une communication sécurisée et authentifiée dans les écosystèmes IoT.

Le rôle des AC intermédiaires
La délégation de la délivrance des certificats aux AC intermédiaires améliore la scalabilité et limite l'exposition.

La confiance est construite sur des hiérarchies
La confiance entre les appareils et les serveurs repose sur des AC racines partagées et des chaînes de certificats validées.

Construisez pour apprendre, utilisez des services gérés pour la production
Bien que cette API soit un excellent outil d'apprentissage, des services comme AWS Private CA ou Let’s Encrypt sont mieux adaptés à la production.

Pour obtenir le code source complet et le déployer vous-même, visitez Serverless-Handbook Self Service IoT Certificate management

Derniers mots

N'oubliez pas de me suivre sur LinkedIn et X pour plus de contenu, et lisez le reste de mes Blogs

Comme le dit Werner ! Maintenant, allez construire !