Verifiera att Azure Confidential Ledger skriver transaktionskvitton

Ett kvitto på en azure confidential ledger-skrivningstransaktion representerar ett kryptografiskt Merkle-bevis på att motsvarande skrivtransaktion har genomförts globalt av CCF-nätverket. Azure Confidential Ledger-användare kan få ett kvitto över en bekräftad skrivtransaktion när som helst för att verifiera att motsvarande skrivåtgärd har registrerats i den oföränderliga transaktionsregistret.

Mer information om att skriva transaktionskvitton för Azure Confidential Ledger finns i den dedikerade artikeln.

Kvittoverifieringssteg

Ett skrivtransaktionskvitto kan verifieras enligt en specifik uppsättning steg som beskrivs i följande underavsnitt. Samma steg beskrivs i CCF-dokumentationen.

Beräkning av lövnoder

Det första steget är att beräkna SHA-256-hashen för lövnoden i Merkle-trädet som motsvarar den checkade transaktionen. En lövnod består av den ordnade sammanlänkningen av följande fält som finns i ett Azure Confidential Ledger-kvitto under leafComponents:

  1. writeSetDigest
  2. SHA-256 sammandrag av commitEvidence
  3. claimsDigest Fält

Dessa värden måste sammanfogas som matriser med byte: båda writeSetDigest och claimsDigest skulle behöva konverteras från strängar med hexadecimala siffror till matriser med byte. Å andra sidan kan hashen commitEvidence för (som en matris med byte) hämtas genom att använda sha-256-hashfunktionen över den UTF-8-kodade commitEvidence strängen.

På samma sätt kan lövnodens hashsammandrag beräknas genom att funktionen SHA-256 hash tillämpas över resultatsammanfogningen av resulterande byte.

Beräkning av rotnoder

Det andra steget är att beräkna SHA-256-hashen för roten i Merkle-trädet när transaktionen checkades in. Beräkningen görs genom att iterativt sammanfoga och hasha resultatet av den tidigare iterationen (från den lövnodshash som beräknades i föregående steg) med de sorterade nodernas hashvärden som anges i proof fältet för ett kvitto. Listan proof tillhandahålls som en ordnad lista och dess element måste itereras i den angivna ordningen.

Sammanfogningen måste göras på byterepresentationen med avseende på den relativa ordning som anges i de objekt som anges i proof fältet (antingen left eller right).

  • Om nyckeln för det aktuella elementet i proof är leftska resultatet av den tidigare iterationen läggas till i det aktuella elementvärdet.
  • Om nyckeln för det aktuella elementet i proof är rightbör resultatet av den tidigare iterationen läggas till i det aktuella elementvärdet.

Efter varje sammanfogning måste funktionen SHA-256 tillämpas för att få indata för nästa iteration. Den här processen följer standardstegen för att beräkna rotnoden i en Merkle Tree-datastruktur med tanke på de noder som krävs för beräkningen.

Verifiera signaturen över rotnoden

Det tredje steget är att kontrollera att den kryptografiska signaturen som skapas via rotnodshashen är giltig med hjälp av certifikatet för signeringsnoden på kvittot. Verifieringsprocessen följer standardstegen för verifiering av digital signatur för meddelanden som signeras med hjälp av ECDSA (Elliptic Curve Digital Signature Algorithm). Mer specifikt är stegen:

  1. Avkoda base64-strängen signature till en matris med byte.
  2. Extrahera den offentliga ECDSA-nyckeln från certifikatet för signeringsnoden cert.
  3. Kontrollera att signaturen över roten för Merkle-trädet (beräknas med hjälp av anvisningarna i föregående underavsnitt) är autentisering med hjälp av den extraherade offentliga nyckeln från föregående steg. Det här steget motsvarar effektivt en standardverifieringsprocess för digitala signaturer med ECDSA. Det finns många bibliotek i de mest populära programmeringsspråken som gör det möjligt att verifiera en ECDSA-signatur med hjälp av ett offentligt nyckelcertifikat över vissa data (till exempel kryptografibiblioteket för Python).

Verifiera godkännande av signeringsnodcertifikat

Förutom föregående steg måste du även kontrollera att certifikatet för signeringsnoden har godkänts (dvs. signerat) av det aktuella transaktionsregistercertifikatet. Det här steget beror inte på de andra tre föregående stegen och kan utföras oberoende av de andra.

Det är möjligt att den aktuella tjänstidentiteten som utfärdade kvittot skiljer sig från den som godkände signeringsnoden (till exempel på grund av en certifikatförnyelse). I det här fallet är det nödvändigt att verifiera kedjan med certifikatförtroende från certifikatet för signeringsnoden (dvs cert . fältet i kvittot) upp till den betrodda rotcertifikatutfärdare (CA) (dvs. det aktuella tjänstidentitetscertifikatet) via andra tidigare tjänstidentiteter (dvs serviceEndorsements . listfältet på kvittot). Listan serviceEndorsements tillhandahålls som en ordnad lista från den äldsta till den senaste tjänstidentiteten.

Certifikatgodkännande måste verifieras för hela kedjan och följer exakt samma verifieringsprocess för digitala signaturer som beskrivs i föregående underavsnitt. Det finns populära kryptografiska bibliotek med öppen källkod (till exempel OpenSSL) som vanligtvis kan användas för att utföra ett certifikatgodkännandesteg.

Verifiera programanspråksammandrag

Om programanspråk är kopplade till ett kvitto är det ett valfritt steg att beräkna anspråkens sammandrag från de exponerade anspråken (efter en specifik algoritm) och kontrollera att sammanfattningen matchar den claimsDigest som finns i kvittots nyttolast. För att beräkna sammandraget från de exponerade anspråksobjekten måste det iterera genom varje programanspråksobjekt i listan och kontrollera dess kind fält.

Om anspråksobjektet är av typen LedgerEntryska transaktionsregistrets samlings-ID (collectionId) och innehållet (contents) i anspråket extraheras och användas för att beräkna sina HMAC-sammandrag med hjälp av den hemliga nyckel (secretKey) som anges i anspråksobjektet. Dessa två sammandrag sammanfogas sedan och SHA-256-hashen för sammanfogningen beräknas. Protokollet (protocol) och den resulterande sammanfattningen av anspråksdata sammanfogas sedan och en annan SHA-256-hash av sammanfogningen beräknas för att få den slutliga sammandraget.

Om anspråksobjektet är av typen ClaimDigestska anspråkssammandraget (value) extraheras, sammanfogas med protokollet (protocol) och SHA-256-hashen för sammanfogningen beräknas för att få den slutliga sammandraget.

När du har beräknat varje enskilt anspråkssammandrag är det nödvändigt att sammanfoga alla beräknade sammandrag från varje programanspråksobjekt (i samma ordning som de visas på kvittot). Sammanfogningen bör sedan förberedas med antalet bearbetade anspråk. SHA-256-hashen för den tidigare sammanfogningen genererar den slutliga sammanfattningen av anspråk, som ska matcha presenten claimsDigest i kvittot.

Fler resurser

Mer information om innehållet i en Azure Confidential Ledger-skrivning av transaktionskvitto och förklaring av varje fält finns i den dedikerade artikeln. CCF-dokumentationen innehåller också mer information om kvittoverifiering och andra relaterade resurser på följande länkar:

Verifiera skrivning av transaktionskvitton

Verktyg för kvittoverifiering

Azure Confidential Ledger-klientbiblioteket för Python innehåller verktygsfunktioner för att verifiera skrivningstransaktionskvitton och beräkna anspråkens sammandrag från en lista över programanspråk. Mer information om hur du använder Data Plane SDK och de kvittospecifika verktygen finns i det här avsnittet och den här exempelkoden.

Konfiguration och förutsättningar

I referenssyfte tillhandahåller vi exempelkod i Python för att fullständigt verifiera att Azure Confidential Ledger skriver transaktionskvitton enligt stegen i föregående avsnitt.

För att köra den fullständiga verifieringsalgoritmen krävs det aktuella tjänstnätverkscertifikatet och ett kvitto på skrivtransaktionen från en konfidentiell transaktionsresurs som körs. I den här artikeln finns information om hur du hämtar ett kvitto för skrivtransaktioner och tjänstcertifikatet från en instans av en konfidentiell transaktionsregister.

Steg-för-steg-beskrivning av kod

Följande kod kan användas för att initiera nödvändiga objekt och köra algoritmen för kvittoverifiering. Ett separat verktyg (verify_receipt) används för att köra den fullständiga verifieringsalgoritmen och accepterar innehållet receipt i fältet i ett GET_RECEIPT svar som en ordlista och tjänstcertifikatet som en enkel sträng. Funktionen utlöser ett undantag om kvittot inte är giltigt eller om något fel påträffades under bearbetningen.

Det antas att både kvittot och tjänstcertifikatet kan läsas in från filer. Se till att uppdatera både konstanterna service_certificate_file_name och receipt_file_name med respektive filnamn för tjänstcertifikatet och det kvitto som du vill verifiera.

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 

Eftersom verifieringsprocessen kräver vissa kryptografiska och hashande primitiver används följande bibliotek för att underlätta beräkningen.

  • CCF Python-biblioteket: modulen innehåller en uppsättning verktyg för kvittoverifiering.
  • Python-kryptografibiblioteket: ett bibliotek som används ofta och som innehåller olika kryptografiska algoritmer och primitiver.
  • Hashlib-modulen, en del av Python-standardbiblioteket: en modul som tillhandahåller ett gemensamt gränssnitt för populära hash-algoritmer.
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 

verify_receipt I funktionen kontrollerar vi att det angivna kvittot är giltigt och innehåller alla obligatoriska fält.

# 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 

Vi initierar de variabler som ska användas i resten av programmet.

# 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"] 

Vi kan läsa in PEM-certifikaten för tjänstidentiteten, signeringsnoden och bekräftelsecertifikaten från tidigare tjänstidentiteter med hjälp av kryptografibiblioteket.

# 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 
] 

Det första steget i verifieringsprocessen är att beräkna sammandraget av lövnoden.

# 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 
)

Funktionen compute_leaf_node accepterar som parametrar lövkomponenterna i kvittot ( claimsDigest, , commitEvidenceoch ) och writeSetDigestreturnerar lövnodshashen i hexadecimal form.

Som vi beskrev tidigare beräknar vi sammandraget av commitEvidence (med funktionen SHA-256 hashlib ). Sedan konverterar vi både writeSetDigest och claimsDigest till matriser med byte. Slutligen sammanfogar vi de tre matriserna och sammanfattar resultatet med hjälp av funktionen 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() 

När vi har beräknat bladet kan vi beräkna roten i Merkle-trädet.

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

Vi använder funktionen root som tillhandahålls som en del av CCF Python-biblioteket. Funktionen sammanfogar successivt resultatet av den tidigare iterationen med ett nytt element från proof, sammanfattar sammanlänkningen och upprepar sedan steget för varje element i proof med den tidigare beräknade sammandraget. Sammanfogningen måste respektera ordningen på noderna i Merkle-trädet för att säkerställa att roten är korrekt omberäknad.

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() 

När rotnodshashen har beräknats kan vi verifiera signaturen som finns i kvittot över roten för att verifiera att signaturen är korrekt.

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

På samma sätt tillhandahåller CCF-biblioteket en funktion verify för att utföra den här verifieringen. Vi använder den offentliga ECDSA-nyckeln för signeringsnodcertifikatet för att verifiera signaturen över trädets rot.

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())), 
    )

Det sista steget i kvittoverifieringen är att verifiera certifikatet som användes för att signera roten i Merkle-trädet.

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

På samma sätt kan vi använda CCF-verktyget check_endorsements för att verifiera att tjänstidentiteten godkänner signeringsnoden. Certifikatkedjan kan bestå av tidigare tjänstcertifikat, så vi bör verifiera att bekräftelsen tillämpas transitivt om serviceEndorsements den inte är en tom lista.

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) 

Alternativt kan vi också verifiera certifikatet med hjälp av OpenSSL-biblioteket med en liknande metod.

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() 

Exempelkod

Den fullständiga exempelkoden som används i kodgenomgången tillhandahålls.

Huvudprogram

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 

Kvittoverifiering

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() 

Nästa steg