Vérifier les reçus de transaction d’écriture du registre confidentiel Azure

Un reçu de transaction d’écriture du registre confidentiel Azure représente une preuve Merkle de chiffrement que la transaction d’écriture correspondante a été validée globalement par le réseau CCF. Les utilisateurs du registre confidentiel Azure peuvent obtenir un reçu sur une transaction d’écriture validée à tout moment pour vérifier que l’opération d’écriture correspondante a été correctement enregistrée dans le registre immuable.

Pour plus d’informations sur les reçus de transaction d’écriture du registre confidentiel Azure, consultez l’article dédié.

Étapes de vérification d’un reçu

Un reçu de transaction d’écriture peut être vérifié en suivant un ensemble spécifique d’étapes décrites dans les sous-sections suivantes. Les mêmes étapes sont décrites dans la documentation du CCF.

Calcul du nœud terminal

La première étape consiste à calculer le code de hachage SHA-256 du nœud terminal dans l’arborescence Merkle correspondant à la transaction validée. Un nœud terminal est constitué de la concaténation ordonnée des champs suivants qui se trouvent dans un reçu du registre confidentiel Azure, sous leafComponents :

  1. writeSetDigest
  2. Synthèse SHA-256 de commitEvidence
  3. Champs claimsDigest

Ces valeurs ont besoin d’être concaténées sous forme de tableaux d’octets : writeSetDigest et claimsDigest doivent être convertis de chaînes de chiffres hexadécimaux en tableaux d’octets. Par ailleurs, le code de hachage de commitEvidence (sous forme de tableau d’octets) peut être obtenu en appliquant la fonction de code de hachage SHA-256 sur la chaîne commitEvidence encodée en UTF-8.

De même, la synthèse du nœud terminal peut être calculée en appliquant la fonction de code de hachage SHA-256 sur la concaténation du résultat des octets obtenus.

Calcul du nœud racine

La deuxième étape consiste à calculer le code de hachage SHA-256 de la racine de l’arborescence Merkle au moment où la transaction a été validée. Ce calcul est effectué en concaténant et en hachant de manière itérative le résultat de l’itération précédente (à partir du code de hachage du nœud terminal calculé à l’étape précédente) avec les codes de hachage des nœuds ordonnés fournis dans le champ proof d’un reçu. La liste proof est fournie sous forme de liste triée et ses éléments doivent être itérés dans l’ordre donné.

La concaténation doit être effectuée sur la représentation en octets par rapport à l’ordre relatif indiqué dans les objets fournis dans le champ proof (left ou right).

  • Si la clé de l’élément actuel dans proof est left, le résultat de l’itération précédente doit être ajouté à la fin de la valeur de l’élément actuel.
  • Si la clé de l’élément actuel dans proof est right, le résultat de l’itération précédente doit être ajouté au début de la valeur de l’élément actuel.

Après chaque concaténation, la fonction SHA-256 doit être appliquée afin d’obtenir l’entrée de l’itération suivante. Ce processus suit les étapes standard pour calculer le nœud racine de la structure de données d’une arborescence Merkle en fonction des nœuds nécessaires pour le calcul.

Vérifier la signature sur le nœud racine

La troisième étape consiste à vérifier que la signature de chiffrement produite sur le code de hachage du nœud racine est valide à l’aide du certificat du nœud de signature inclus dans le reçu. Le processus de vérification suit les étapes standard de vérification d’une signature numérique pour des messages signés à l’aide de l’algorithme de signature numérique à courbe elliptique (ECDSA, Elliptic Curve Digital Signature Algorithm). Plus précisément, les étapes sont les suivantes :

  1. Décodez la chaîne en base64 signature dans un tableau d’octets.
  2. Extrayez la clé publique ECDSA du certificat du nœud de signature cert.
  3. Vérifiez que la signature sur la racine de l’arborescence Merkle (calculée à l’aide des instructions données dans la sous-section précédente) est authentique à l’aide de la clé publique extraite de l’étape précédente. Cette étape correspond effectivement à un processus standard de vérification d’une signature numérique à l’aide de l’algorithme ECDSA. Il existe de nombreuses bibliothèques dans les langages de programmation les plus courants qui permettent de vérifier une signature ECDSA à l’aide d’un certificat de clé publique sur certaines données (par exemple, la bibliothèque de chiffrement pour Python).

Vérifier l’approbation du certificat du nœud de signature

Outre l’étape précédente, il est également nécessaire de vérifier que le certificat de nœud de signature est approuvé (c’est-à-dire signé) par le certificat de registre actuel. Cette étape ne dépend pas des trois autres étapes précédentes, vous pouvez l’effectuer indépendamment des autres.

Il est possible que l’identité de service actuelle qui a émis le reçu soit différente de celle qui a approuvé le nœud de signature (par exemple, en raison d’un renouvellement de certificat). Dans ce cas, il est nécessaire de vérifier la chaîne d’approbation des certificats à partir du certificat du nœud de signature (c’est-à-dire, le champ cert dans le reçu) jusqu’à l’autorité de certification racine approuvée (autrement dit, le certificat d’identité de service actuel) par le biais des autres identités de service précédentes (autrement dit, le champ de liste serviceEndorsements inclus dans le reçu). La liste serviceEndorsements est fournie sous la forme d’une liste triée de la plus ancienne à la plus récente identité de service.

L’approbation du certificat a besoin d’être vérifiée pour l’ensemble de la chaîne et elle suit exactement le même processus de vérification d’une signature numérique que celui décrit dans la sous-section précédente. Il existe des bibliothèques de chiffrement open source populaires (par exemple, OpenSSL) qui peuvent généralement être utilisées pour effectuer une étape d’approbation de certificat.

Vérifier la synthèse des revendications d’application

En guise d’étape facultative, si les revendications d’application sont attachées à un reçu, il est possible de calculer le résumé des revendications à partir des revendications exposées (suivant un algorithme spécifique) et de vérifier que le digest correspond au claimsDigest contenu de la charge utile de réception. Pour calculer la synthèse à partir des objets de revendication exposés, il est nécessaire d’itérer à travers chaque objet de revendication d’application dans la liste et de case activée son kind champ.

Si l’objet de revendication est de type LedgerEntry, l’ID de collection de registre (collectionId) et le contenu (contents) de la revendication doivent être extraits et utilisés pour calculer leurs synthèses HMAC à l’aide de la clé secrète (secretKey) spécifiée dans l’objet de revendication. Ces deux synthèses sont ensuite concaténées et le hachage SHA-256 de la concaténation est calculé. Le protocole (protocol) et la synthèse des données de revendication résultantes sont ensuite concaténés et un autre hachage SHA-256 de la concaténation est calculé pour obtenir la synthèse finale.

Si l’objet de revendication est de type ClaimDigest, le digest de revendication (value) doit être extrait, concaténé avec le protocole (protocol) et le hachage SHA-256 de la concaténation est calculé pour obtenir le digest final.

Après avoir calculé chaque synthèse de revendication unique, il est nécessaire de concaténer toutes les synthèses calculées de chaque objet de revendication d’application (dans le même ordre qu’ils sont présentés dans le reçu). La concaténation doit ensuite être précédée du nombre de revendications traitées. Le hachage SHA-256 de la concaténation précédente produit le résumé des revendications finales, qui doit correspondre au claimsDigest présent dans l’objet de réception.

Plus de ressources

Pour plus d’informations sur le contenu d’un reçu de transaction d’écriture du registre confidentiel Azure et pour obtenir une explication de chaque champ, consultez l’article dédié. La documentation du CCF contient aussi d’autres informations sur la vérification des reçus et d’autres ressources connexes sont disponibles par le biais des liens suivants :

Vérifier les reçus de transaction écrite

Utilitaires de vérification des reçus

La bibliothèque cliente du registre confidentiel Azure pour Python fournit des fonctions utilitaires pour vérifier les reçus de transaction en écriture et calculer le résumé des revendications à partir d’une liste de revendications d’application. Pour plus d’informations sur l’utilisation du Kit de développement logiciel (SDK) de plan de données et des utilitaires spécifiques aux reçus, consultez cette section et cet exemple de code.

Configuration et prérequis

À des fins de référence, nous fournissons un exemple de code en Python pour vérifier entièrement les reçus de transaction d’écriture du registre confidentiel Azure en suivant les étapes décrites dans la section précédente.

Pour exécuter l’algorithme de vérification complète, le certificat réseau du service actuel et un reçu de transaction d’écriture provenant d’une ressource du registre confidentiel en cours d’exécution sont nécessaires. Reportez-vous à cet article pour plus d’informations sur la façon d’extraire un reçu de transaction d’écriture et le certificat de service à partir d’une instance du registre confidentiel.

Vérification du code

Le code suivant peut être utilisé pour initialiser les objets nécessaires et exécuter l’algorithme de vérification d’un reçu. Un utilitaire distinct (verify_receipt) est utilisé pour exécuter l’algorithme de vérification complète et accepte le contenu du receipt champ dans une GET_RECEIPT réponse en tant que dictionnaire et le certificat de service sous forme de chaîne simple. La fonction lève une exception si le reçu n’est pas valide ou si une erreur a été rencontrée pendant le traitement.

Nous partons du principe que le reçu et le certificat de service peuvent être chargés à partir de fichiers. Veillez à mettre à jour les constantes service_certificate_file_name et receipt_file_name avec les noms de fichiers respectifs du certificat de service et du reçu que vous voulez vérifier.

import json 

# Constants
service_certificate_file_name = "<your-service-certificate-file>"
receipt_file_name = "<your-receipt-file>"

# Use the receipt and the service identity to verify the receipt content 
with open(service_certificate_file_name, "r") as service_certificate_file, open( 
    receipt_file_name, "r" 
) as receipt_file: 

    # Load relevant files content 
    receipt = json.loads(receipt_file.read())["receipt"] 
    service_certificate_cert = service_certificate_file.read() 

    try: 
        verify_receipt(receipt, service_certificate_cert) 
        print("Receipt verification succeeded") 

    except Exception as e: 
        print("Receipt verification failed") 

        # Raise caught exception to look at the error stack
        raise e 

Comme le processus de vérification nécessite des primitives de chiffrement et de hachage, les bibliothèques suivantes sont utilisées pour faciliter le calcul.

  • Bibliothèque Python du CCF : module qui fournit un ensemble d’outils pour la vérification des reçus.
  • Bibliothèque de chiffrement Python : bibliothèque largement utilisée qui inclut divers algorithmes de chiffrement et primitives.
  • Module hashlib faisant partie de la bibliothèque standard Python : module qui fournit une interface commune pour les algorithmes de hachage connus.
from ccf.receipt import verify, check_endorsements, root 
from cryptography.x509 import load_pem_x509_certificate, Certificate 
from hashlib import sha256 
from typing import Dict, List, Any 

À l’intérieur de la fonction verify_receipt, nous vérifions que le reçu donné est valide et qu’il contient tous les champs nécessaires.

# Check that all the fields are present in the receipt 
assert "cert" in receipt 
assert "leafComponents" in receipt 
assert "claimsDigest" in receipt["leafComponents"] 
assert "commitEvidence" in receipt["leafComponents"] 
assert "writeSetDigest" in receipt["leafComponents"] 
assert "proof" in receipt 
assert "signature" in receipt 

Nous initialisons les variables qui vont être utilisées dans le reste du programme.

# Set the variables 
node_cert_pem = receipt["cert"] 
claims_digest_hex = receipt["leafComponents"]["claimsDigest"] 
commit_evidence_str = receipt["leafComponents"]["commitEvidence"] 
write_set_digest_hex = receipt["leafComponents"]["writeSetDigest"] 
proof_list = receipt["proof"] 
service_endorsements_certs_pem = receipt.get("serviceEndorsements", [])
root_node_signature = receipt["signature"] 

Nous pouvons charger les certificats PEM pour l’identité de service, le nœud de signature et les certificats d’approbation des identités de service précédentes à l’aide de la bibliothèque de chiffrement.

# Load service and node PEM certificates 
service_cert = load_pem_x509_certificate(service_cert_pem.encode()) 
node_cert = load_pem_x509_certificate(node_cert_pem.encode()) 

# Load service endorsements PEM certificates 
service_endorsements_certs = [ 
    load_pem_x509_certificate(pem.encode()) 
    for pem in service_endorsements_certs_pem 
] 

La première étape du processus de vérification consiste à calculer la synthèse du nœud terminal.

# Compute leaf of the Merkle Tree corresponding to our transaction 
leaf_node_hex = compute_leaf_node( 
    claims_digest_hex, commit_evidence_str, write_set_digest_hex 
)

La fonction compute_leaf_node accepte en tant que paramètres les composants terminaux du reçu (claimsDigest, commitEvidence et writeSetDigest) et retourne le code de hachage du nœud terminal sous forme hexadécimale.

Comme indiqué précédemment, nous calculons la synthèse de (à l’aide de commitEvidence la fonction SHA-256 hashlib ). Ensuite, nous convertissons à la fois writeSetDigest et claimsDigest en tableaux d’octets. Enfin, nous concaténons les trois tableaux et nous synthétisons le résultat à l’aide de la fonction SHA256.

def compute_leaf_node( 
    claims_digest_hex: str, commit_evidence_str: str, write_set_digest_hex: str 
) -> str: 
    """Function to compute the leaf node associated to a transaction 
    given its claims digest, commit evidence, and write set digest.""" 

    # Digest commit evidence string 
    commit_evidence_digest = sha256(commit_evidence_str.encode()).digest() 

    # Convert write set digest to bytes 
    write_set_digest = bytes.fromhex(write_set_digest_hex) 

    # Convert claims digest to bytes 
    claims_digest = bytes.fromhex(claims_digest_hex) 

    # Create leaf node by hashing the concatenation of its three components 
    # as bytes objects in the following order: 
    # 1. write_set_digest 
    # 2. commit_evidence_digest 
    # 3. claims_digest 
    leaf_node_digest = sha256( 
        write_set_digest + commit_evidence_digest + claims_digest 
    ).digest() 

    # Convert the result into a string of hexadecimal digits 
    return leaf_node_digest.hex() 

Après avoir calculé le terminal, nous pouvons calculer la racine de l’arborescence Merkle.

# Compute root of the Merkle Tree 
root_node = root(leaf_node_hex, proof_list) 

Nous utilisons la fonction root fournie dans le cadre de la bibliothèque Python du CCF. La fonction concatène successivement le résultat de l’itération précédente avec un nouvel élément issu de proof, synthétise la concaténation, puis répète l’étape pour chaque élément inclus dans proof avec la synthèse calculée précédemment. La concaténation doit respecter l’ordre des nœuds dans l’arborescence Merkle pour veiller à ce que la racine soit correctement recalculée.

def root(leaf: str, proof: List[dict]): 
    """ 
    Recompute root of Merkle tree from a leaf and a proof of the form: 
    [{"left": digest}, {"right": digest}, ...] 
    """ 

    current = bytes.fromhex(leaf) 

    for n in proof: 
        if "left" in n: 
            current = sha256(bytes.fromhex(n["left"]) + current).digest() 
        else: 
            current = sha256(current + bytes.fromhex(n["right"])).digest() 
    return current.hex() 

Après avoir calculé le code de hachage du nœud racine, nous pouvons vérifier la signature contenue dans le reçu par rapport à la racine pour confirmer que la signature est correcte.

# Verify signature of the signing node over the root of the tree 
verify(root_node, root_node_signature, node_cert) 

Là encore, la bibliothèque du CCF fournit une fonction verify pour effectuer cette vérification. Nous utilisons la clé publique ECDSA du certificat du nœud de signature pour vérifier la signature par rapport à la racine de l’arborescence.

def verify(root: str, signature: str, cert: Certificate):
    """ 
    Verify signature over root of Merkle Tree 
    """ 

    sig = base64.b64decode(signature) 
    pk = cert.public_key() 
    assert isinstance(pk, ec.EllipticCurvePublicKey) 
    pk.verify( 
        sig, 
        bytes.fromhex(root), 
        ec.ECDSA(utils.Prehashed(hashes.SHA256())), 
    )

La dernière étape de la vérification d’un reçu consiste à valider le certificat utilisé pour signer la racine de l’arborescence Merkle.

# Verify node certificate is endorsed by the service certificates through endorsements 
check_endorsements(node_cert, service_cert, service_endorsements_certs) 

De même, nous pouvons utiliser l’utilitaire check_endorsements CCF pour vérifier que l’identité de service approuve le nœud de signature. La chaîne de certificats pouvant être composée de certificats de service précédents, nous devons confirmer que l’approbation est appliquée de manière transitive si serviceEndorsements n’est pas une liste vide.

def check_endorsement(endorsee: Certificate, endorser: Certificate): 
    """ 
    Check endorser has endorsed endorsee 
    """ 

    digest_algo = endorsee.signature_hash_algorithm 
    assert digest_algo 
    digester = hashes.Hash(digest_algo) 
    digester.update(endorsee.tbs_certificate_bytes) 
    digest = digester.finalize() 
    endorser_pk = endorser.public_key() 
    assert isinstance(endorser_pk, ec.EllipticCurvePublicKey) 
    endorser_pk.verify( 
        endorsee.signature, digest, ec.ECDSA(utils.Prehashed(digest_algo)) 
    ) 

def check_endorsements( 
    node_cert: Certificate, service_cert: Certificate, endorsements: List[Certificate] 
): 
    """ 
    Check a node certificate is endorsed by a service certificate, transitively through a list of endorsements. 
    """ 

    cert_i = node_cert 
    for endorsement in endorsements: 
        check_endorsement(cert_i, endorsement) 
        cert_i = endorsement 
    check_endorsement(cert_i, service_cert) 

En guise d’alternative, nous pouvons aussi valider le certificat à l’aide de la bibliothèque OpenSSL en utilisant une méthode similaire.

from OpenSSL.crypto import ( 
    X509, 
    X509Store, 
    X509StoreContext, 
)

def verify_openssl_certificate( 
    node_cert: Certificate, 
    service_cert: Certificate, 
    service_endorsements_certs: List[Certificate], 
) -> None: 
    """Verify that the given node certificate is a valid OpenSSL certificate through 
    the service certificate and a list of endorsements certificates.""" 

    store = X509Store() 

    # pyopenssl does not support X509_V_FLAG_NO_CHECK_TIME. For recovery of expired 
    # services and historical receipts, we want to ignore the validity time. 0x200000 
    # is the bitmask for this option in more recent versions of OpenSSL. 
    X509_V_FLAG_NO_CHECK_TIME = 0x200000 
    store.set_flags(X509_V_FLAG_NO_CHECK_TIME) 

    # Add service certificate to the X.509 store 
    store.add_cert(X509.from_cryptography(service_cert)) 

    # Prepare X.509 endorsement certificates 
    certs_chain = [X509.from_cryptography(cert) for cert in service_endorsements_certs] 

    # Prepare X.509 node certificate 
    node_cert_pem = X509.from_cryptography(node_cert) 

    # Create X.509 store context and verify its certificate 
    ctx = X509StoreContext(store, node_cert_pem, certs_chain) 
    ctx.verify_certificate() 

Exemple de code

L’exemple de code complet utilisé dans la procédure pas à pas est fourni.

Programme principal

import json 

# Use the receipt and the service identity to verify the receipt content 
with open("network_certificate.pem", "r") as service_certificate_file, open( 
    "receipt.json", "r" 
) as receipt_file: 

    # Load relevant files content 
    receipt = json.loads(receipt_file.read())["receipt"]
    service_certificate_cert = service_certificate_file.read()

    try: 
        verify_receipt(receipt, service_certificate_cert) 
        print("Receipt verification succeeded") 

    except Exception as e: 
        print("Receipt verification failed") 

        # Raise caught exception to look at the error stack 
        raise e 

Vérification d’un reçu

from cryptography.x509 import load_pem_x509_certificate, Certificate 
from hashlib import sha256 
from typing import Dict, List, Any 

from OpenSSL.crypto import ( 
    X509, 
    X509Store, 
    X509StoreContext, 
) 

from ccf.receipt import root, verify, check_endorsements 

def verify_receipt(receipt: Dict[str, Any], service_cert_pem: str) -> None: 
    """Function to verify that a given write transaction receipt is valid based 
    on its content and the service certificate. 
    Throws an exception if the verification fails.""" 

    # Check that all the fields are present in the receipt 
    assert "cert" in receipt 
    assert "leafComponents" in receipt 
    assert "claimsDigest" in receipt["leafComponents"] 
    assert "commitEvidence" in receipt["leafComponents"] 
    assert "writeSetDigest" in receipt["leafComponents"] 
    assert "proof" in receipt 
    assert "signature" in receipt 

    # Set the variables 
    node_cert_pem = receipt["cert"] 
    claims_digest_hex = receipt["leafComponents"]["claimsDigest"] 
    commit_evidence_str = receipt["leafComponents"]["commitEvidence"] 

    write_set_digest_hex = receipt["leafComponents"]["writeSetDigest"] 
    proof_list = receipt["proof"] 
    service_endorsements_certs_pem = receipt.get("serviceEndorsements", [])
    root_node_signature = receipt["signature"] 

    # Load service and node PEM certificates
    service_cert = load_pem_x509_certificate(service_cert_pem.encode()) 
    node_cert = load_pem_x509_certificate(node_cert_pem.encode()) 

    # Load service endorsements PEM certificates
    service_endorsements_certs = [ 
        load_pem_x509_certificate(pem.encode()) 
        for pem in service_endorsements_certs_pem 
    ] 

    # Compute leaf of the Merkle Tree 
    leaf_node_hex = compute_leaf_node( 
        claims_digest_hex, commit_evidence_str, write_set_digest_hex 
    ) 

    # Compute root of the Merkle Tree
    root_node = root(leaf_node_hex, proof_list) 

    # Verify signature of the signing node over the root of the tree
    verify(root_node, root_node_signature, node_cert) 

    # Verify node certificate is endorsed by the service certificates through endorsements
    check_endorsements(node_cert, service_cert, service_endorsements_certs) 

    # Alternative: Verify node certificate is endorsed by the service certificates through endorsements 
    verify_openssl_certificate(node_cert, service_cert, service_endorsements_certs) 

def compute_leaf_node( 
    claims_digest_hex: str, commit_evidence_str: str, write_set_digest_hex: str 
) -> str: 
    """Function to compute the leaf node associated to a transaction 
    given its claims digest, commit evidence, and write set digest.""" 

    # Digest commit evidence string
    commit_evidence_digest = sha256(commit_evidence_str.encode()).digest() 

    # Convert write set digest to bytes
    write_set_digest = bytes.fromhex(write_set_digest_hex) 

    # Convert claims digest to bytes
    claims_digest = bytes.fromhex(claims_digest_hex) 

    # Create leaf node by hashing the concatenation of its three components 
    # as bytes objects in the following order: 
    # 1. write_set_digest 
    # 2. commit_evidence_digest 
    # 3. claims_digest 
    leaf_node_digest = sha256( 
        write_set_digest + commit_evidence_digest + claims_digest 
    ).digest() 

    # Convert the result into a string of hexadecimal digits 
    return leaf_node_digest.hex() 

def verify_openssl_certificate( 
    node_cert: Certificate, 
    service_cert: Certificate, 
    service_endorsements_certs: List[Certificate], 
) -> None: 
    """Verify that the given node certificate is a valid OpenSSL certificate through 
    the service certificate and a list of endorsements certificates.""" 

    store = X509Store() 

    # pyopenssl does not support X509_V_FLAG_NO_CHECK_TIME. For recovery of expired 
    # services and historical receipts, we want to ignore the validity time. 0x200000 
    # is the bitmask for this option in more recent versions of OpenSSL. 
    X509_V_FLAG_NO_CHECK_TIME = 0x200000 
    store.set_flags(X509_V_FLAG_NO_CHECK_TIME) 

    # Add service certificate to the X.509 store
    store.add_cert(X509.from_cryptography(service_cert)) 

    # Prepare X.509 endorsement certificates
    certs_chain = [X509.from_cryptography(cert) for cert in service_endorsements_certs] 

    # Prepare X.509 node certificate
    node_cert_pem = X509.from_cryptography(node_cert) 

    # Create X.509 store context and verify its certificate
    ctx = X509StoreContext(store, node_cert_pem, certs_chain) 
    ctx.verify_certificate() 

Étapes suivantes