Partilhar via


Verificar recibos de transação de gravação do Razão Confidencial do Azure

Um recibo de transação de gravação do Razão Confidencial do Azure representa uma prova criptográfica Merkle de que a transação de gravação correspondente foi confirmada globalmente pela rede CCF. Os usuários do Razão Confidencial do Azure podem obter um recibo sobre uma transação de gravação confirmada a qualquer momento para verificar se a operação de gravação correspondente foi registrada com êxito no livro-razão imutável.

Para obter mais informações sobre recibos de transação de gravação do Razão Confidencial do Azure, consulte o artigo dedicado.

Etapas de verificação de recebimento

Um recibo de transação de gravação pode ser verificado seguindo um conjunto específico de etapas descritas nas subseções a seguir. As mesmas etapas são descritas na Documentação do CCF.

Cálculo do nó foliar

O primeiro passo é calcular o hash SHA-256 do nó folha na árvore Merkle correspondente à transação comprometida. Um nó folha é composto pela concatenação ordenada dos seguintes campos que podem ser encontrados em um recibo do Razão Confidencial do Azure, em leafComponents:

  1. writeSetDigest
  2. SHA-256 resumo de commitEvidence
  3. claimsDigest campos

Esses valores precisam ser concatenados como matrizes de bytes: ambos writeSetDigest e claimsDigest precisariam ser convertidos de cadeias de dígitos hexadecimais para matrizes de bytes, por outro lado, o hash de (como uma matriz de bytes) pode ser obtido aplicando a função de commitEvidence hash SHA-256 sobre a cadeia codificada commitEvidence UTF-8.

Da mesma forma, o resumo de hash do nó folha pode ser calculado aplicando a função de hash SHA-256 sobre a concatenação de resultados dos bytes resultantes.

Computação do nó raiz

O segundo passo é calcular o hash SHA-256 da raiz da árvore Merkle no momento em que a transação foi confirmada. O cálculo é feito por concatenação iterativa e hashing do resultado da iteração anterior (a partir do hash do nó folha computado na etapa anterior) com os hashes dos nós ordenados fornecidos no proof campo de um recibo. A proof lista é fornecida como uma lista ordenada e seus elementos precisam ser iterados na ordem dada.

A concatenação precisa ser feita na representação de bytes em relação à ordem relativa indicada nos objetos fornecidos no proof campo (ou left rightou ).

  • Se a chave do elemento atual em proof for left, o resultado da iteração anterior deverá ser acrescentado ao valor do elemento atual.
  • Se a chave do elemento atual em proof for right, o resultado da iteração anterior deverá ser precedido do valor do elemento atual.

Após cada concatenação, a função SHA-256 precisa ser aplicada para obter a entrada para a próxima iteração. Este processo segue as etapas padrão para calcular o nó raiz de uma estrutura de dados Merkle Tree dados os nós necessários para a computação.

Verificar a assinatura no nó raiz

A terceira etapa é verificar se a assinatura criptográfica produzida sobre o hash do nó raiz é válida usando o certificado do nó de assinatura no recibo. O processo de verificação segue as etapas padrão para verificação de assinatura digital para mensagens assinadas usando o Algoritmo de Assinatura Digital de Curva Elíptica (ECDSA). Mais especificamente, as etapas são:

  1. Decodifice a cadeia de caracteres signature base64 em uma matriz de bytes.
  2. Extraia a chave pública ECDSA do certificado certdo nó de assinatura.
  3. Verifique se a assinatura sobre a raiz da árvore Merkle (calculada usando as instruções na subseção anterior) é autêntica usando a chave pública extraída da etapa anterior. Esta etapa corresponde efetivamente a um processo padrão de verificação de assinatura digital usando ECDSA. Existem muitas bibliotecas nas linguagens de programação mais populares que permitem verificar uma assinatura ECDSA usando um certificado de chave pública sobre alguns dados (por exemplo, a biblioteca de criptografia para Python).

Verificar o endosso do certificado do nó de assinatura

Além da etapa anterior, também é necessário verificar se o certificado do nó de assinatura está endossado (ou seja, assinado) pelo certificado do razão atual. Esta etapa não depende das outras três etapas anteriores e pode ser realizada independentemente das outras.

É possível que a identidade de serviço atual que emitiu o recibo seja diferente daquela que endossou o nó de assinatura (por exemplo, devido a uma renovação de certificado). Nesse caso, é necessário verificar a cadeia de certificados confiáveis do certificado do nó de assinatura (ou seja, o cert campo no recibo) até a Autoridade de Certificação (CA) raiz confiável (ou seja, o certificado de identidade de serviço atual) por meio de outras identidades de serviço anteriores (ou seja, o serviceEndorsements campo de lista no recibo). A serviceEndorsements lista é fornecida como uma lista ordenada da identidade de serviço mais antiga para a mais recente.

O endosso do certificado precisa ser verificado para toda a cadeia e segue exatamente o mesmo processo de verificação de assinatura digital descrito na subseção anterior. Existem bibliotecas criptográficas de código aberto populares (por exemplo, OpenSSL) que normalmente podem ser usadas para executar uma etapa de endosso de certificado.

Verificar resumo de declarações de aplicativo

Como etapa opcional, caso as declarações do aplicativo sejam anexadas a um recibo, é possível calcular o resumo das declarações a partir das declarações expostas (seguindo um algoritmo específico) e verificar se o resumo corresponde ao claimsDigest contido na carga útil do recibo. Para calcular o resumo dos objetos de declaração expostos, é necessário iterar através de cada objeto de declaração de aplicativo na lista e verificar seu kind campo.

Se o objeto de declaração for do tipo LedgerEntry, o ID da coleção contábil (collectionId) e o conteúdo (contents) da declaração devem ser extraídos e usados para calcular seus resumos HMAC usando a chave secreta (secretKey) especificada no objeto de declaração. Esses dois resumos são então concatenados e o hash SHA-256 da concatenação é calculado. O protocolo (protocol) e o resumo de dados de declaração resultante são então concatenados e outro hash SHA-256 da concatenação é calculado para obter o resumo final.

Se o objeto da declaração for do tipo ClaimDigest, o resumo da declaração (value) deve ser extraído, concatenado com o protocolo (protocol), e o hash SHA-256 da concatenação é calculado para obter o resumo final.

Depois de calcular cada resumo de declaração individual, é necessário concatenar todos os resumos computados de cada objeto de reivindicação de aplicativo (na mesma ordem em que são apresentados no recibo). A concatenação deve então ser precedida do número de pedidos processados. O hash SHA-256 da concatenação anterior produz o resumo final das declarações, que deve corresponder ao claimsDigest presente no objeto de recibo.

Mais recursos

Para obter mais informações sobre o conteúdo de um recibo de transação de gravação do Razão Confidencial do Azure e explicação de cada campo, consulte o artigo dedicado. A documentação do CCF também contém mais informações sobre verificação de recebimento e outros recursos relacionados nos seguintes links:

Verificar recibos de transação de gravação

Utilitários de verificação de recibo

A biblioteca de cliente do Azure Confidential Ledger para Python fornece funções de utilitário para verificar recibos de transação de gravação e calcular o resumo de declarações a partir de uma lista de declarações de aplicativo. Para obter mais informações sobre como usar o SDK do Plano de Dados e os utilitários específicos de recebimento, consulte esta seção e este código de exemplo.

Configuração e pré-requisitos

Para fins de referência, fornecemos código de exemplo em Python para verificar totalmente os recibos de transação de gravação do Razão Confidencial do Azure seguindo as etapas descritas na seção anterior.

Para executar o algoritmo de verificação completa, o certificado de rede de serviço atual e um recibo de transação de gravação de um recurso de Razão Confidencial em execução são necessários. Consulte este artigo para obter detalhes sobre como buscar um recibo de transação de gravação e o certificado de serviço de uma instância do Razão Confidencial.

Instruções de código

O código a seguir pode ser usado para inicializar os objetos necessários e executar o algoritmo de verificação de recibo. Um utilitário separado (verify_receipt) é usado para executar o algoritmo de verificação completa e aceita o receipt conteúdo do campo em uma GET_RECEIPT resposta como um dicionário e o certificado de serviço como uma cadeia de caracteres simples. A função lança uma exceção se o recibo não for válido ou se algum erro foi encontrado durante o processamento.

Presume-se que tanto o recibo quanto o certificado de serviço podem ser carregados a partir de arquivos. Certifique-se de atualizar as service_certificate_file_name constantes e receipt_file_name com os respetivos nomes de arquivos do certificado de serviço e recibo que você gostaria de verificar.

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 

Como o processo de verificação requer algumas primitivas criptográficas e hashing, as bibliotecas a seguir são usadas para facilitar a computação.

  • A biblioteca CCF Python: o módulo fornece um conjunto de ferramentas para verificação de recebimento.
  • A biblioteca de criptografia Python: uma biblioteca amplamente utilizada que inclui vários algoritmos criptográficos e primitivos.
  • O módulo hashlib, parte da biblioteca padrão do Python: um módulo que fornece uma interface comum para algoritmos de hash populares.
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 

Dentro da verify_receipt função, verificamos se o recibo fornecido é válido e contém todos os campos obrigatórios.

# 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 

Inicializamos as variáveis que serão usadas no resto do programa.

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

Podemos carregar os certificados PEM para a identidade do serviço, o nó de assinatura e os certificados de endosso de identidades de serviço anteriores usando a biblioteca de criptografia.

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

O primeiro passo do processo de verificação é calcular o resumo do nó foliar.

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

A compute_leaf_node função aceita como parâmetros os componentes folha do recibo (o claimsDigest, o commitEvidence, e o writeSetDigest) e retorna o hash do nó folha em forma hexadecimal.

Como detalhado anteriormente, calculamos o resumo de commitEvidence (usando a função SHA-256 hashlib ). Em seguida, convertemos ambos writeSetDigest e claimsDigest em matrizes de bytes. Finalmente, concatenamos as três matrizes e digerimos o resultado usando a função 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() 

Depois de calcular a folha, podemos calcular a raiz da árvore Merkle.

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

Usamos a função root fornecida como parte da biblioteca Python do CCF. A função concatena sucessivamente o resultado da iteração anterior com um novo elemento de proof, digere a concatenação e, em seguida, repete o passo para cada elemento com proof o digesto previamente calculado. A concatenação precisa respeitar a ordem dos nós na árvore Merkle para garantir que a raiz seja recalculada corretamente.

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

Depois de calcular o hash do nó raiz, podemos verificar a assinatura contida no recibo sobre a raiz para validar se a assinatura está correta.

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

Da mesma forma, a biblioteca CCF fornece uma função verify para fazer essa verificação. Usamos a chave pública ECDSA do certificado do nó de assinatura para verificar a assinatura na raiz da árvore.

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

A última etapa da verificação de recebimento é validar o certificado que foi usado para assinar a raiz da árvore Merkle.

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

Da mesma forma, podemos usar o utilitário check_endorsements CCF para validar se a identidade do serviço endossa o nó de assinatura. A cadeia de certificados pode ser composta por certificados de serviço anteriores, portanto, devemos validar se o endosso é aplicado transitivamente se serviceEndorsements não for uma lista vazia.

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) 

Como alternativa, também poderíamos validar o certificado usando a biblioteca OpenSSL usando um método semelhante.

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

Código de exemplo

O código de exemplo completo usado no passo a passo do código é fornecido.

Programa 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 

Verificação de recibo

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

Próximos passos