Verifique os 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 do Merkle de que a transação de gravação correspondente foi confirmada globalmente pela rede da 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 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 do recibo

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 estão descritas na Documentação da CCF.

Computação de nó folha

A primeira etapa é calcular o hash SHA-256 do nó folha na Árvore Merkle correspondente à transação confirmada. 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. Hash SHA-256 de commitEvidence
  3. Campos claimsDigest

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

Da mesma forma, o código hash do nó folha pode ser calculado aplicando a função de hash SHA-256 na concatenação de resultado dos bytes resultantes.

Computação de nó raiz

A segunda etapa é calcular o hash SHA-256 da raiz da Árvore Merkle no momento em que a transação for confirmada. A computação é feita concatenando iterativamente e fazendo hash do resultado da iteração anterior (a partir do hash de nó folha calculado na etapa anterior) com os hashes dos nós ordenados fornecidos no campo proof de um recibo. A lista proof é fornecida como uma lista ordenada e seus elementos precisam ser iterados na ordem determinada.

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

  • Se a chave do elemento atual em proof for left, o resultado da iteração anterior deverá ser anexado ao valor do elemento atual.
  • Se a chave do elemento atual em proof for right, o resultado da iteração anterior deverá ser prefixado ao 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. Esse processo segue as etapas padrão para calcular o nó raiz de uma estrutura de dados da Árvore Merkle, considerando os nós necessários para a computação.

Verificar assinatura no nó raiz

A terceira etapa é verificar se a assinatura criptográfica produzida pelo hash do nó raiz é válida usando o certificado de 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 ECDSA (algoritmo de assinatura digital de curva elíptica). Mais especificamente, as etapas são:

  1. Decodificar a cadeia de caracteres base64 signature em uma matriz de bytes.
  2. Extraia a chave pública ECDSA do certificado do nó de assinatura cert.
  3. Verifique se a assinatura na 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 de verificação de assinatura digital padrão usando ECDSA. Há muitas bibliotecas nas linguagens de programação mais populares que permitem verificar uma assinatura ECDSA usando um certificado de chave pública em 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 livro razão atual. Essa etapa não depende das outras três etapas anteriores e pode ser executada 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 campo cert no recibo) até a AC (autoridade de certificação) 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 campo de lista serviceEndorsements no recibo). A lista serviceEndorsements é fornecida como uma lista ordenada da mais antiga para a identidade de serviço mais recente.

O endosso de 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. Há 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 o resumo de declarações do aplicativo

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

Se o objeto da declaração for do tipo LedgerEntry, a ID da coleção do livro razão (collectionId) e o conteúdo (contents) da declaração deverão ser extraídos e usados para calcular seus resumos de HMAC usando a chave secreta (secretKey) especificada no objeto da declaração. Esses dois resumos são concatenados a seguir e o hash SHA-256 da concatenação é calculado. O protocolo (protocol) e o resumo dos dados de declaração resultantes são concatenados a seguir e um 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) deverá ser extraído, concatenado com o protocolo (protocol) e o hash SHA-256 da concatenação será calculado para obter o resumo final.

Após calcular cada resumo de declaração individual, é necessário concatenar todos os resumos calculados de cada objeto de declaração de aplicativo (na mesma ordem em que são apresentados no recibo). Em seguida, a concatenação deve ser anexada com o número de declarações processadas. O hash SHA-256 da concatenação anterior produz o resumo final de declarações, que deve corresponder ao claimsDigest presente no objeto do 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 a explicação de cada campo, consulte o artigo dedicado. A documentação da CCF também contém mais informações sobre a verificação de recibos e outros recursos relacionados nos seguintes links:

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

Utilitários de verificação de recibos

A biblioteca de cliente do Livro Razão Confidencial do Azure para Python fornece funções de utilitário para verificar a gravação de recibos de transações e calcular o resumo de declarações 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 do recibo, confira essa seção e essa amostra de código.

Configuração e pré-requisitos

Para fins de referência, fornecemos uma mostra de código em Python para verificar totalmente a gravação de recibos de transações do Livro Razão Confidencial do Azure seguindo as etapas descritas acima.

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 do 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.

Passo a passo do código

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

Supõe-se que tanto o recibo quanto o certificado de serviço possam ser carregados de arquivos. Atualize as constantes service_certificate_file_name e receipt_file_name com os respectivos nomes de arquivos do certificado de serviço e do 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 alguns primitivos de criptografia e hash, as bibliotecas a seguir são usadas para facilitar a computação.

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 função verify_receipt, verificamos se o recibo especificado é válido e contém todos os campos necessá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 restante 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 
] 

A primeira etapa do processo de verificação é calcular o resumo do nó folha.

# 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 função compute_leaf_node aceita como parâmetros os componentes folha do recibo (o claimsDigest, o commitEvidence e o writeSetDigest) e retorna o hash de nó folha na forma hexadecimal.

Conforme detalhado anteriormente, calculamos o resumo de commitEvidence (usando a função hashlib do SHA-256). Em seguida, convertemos writeSetDigest e claimsDigest em matrizes de bytes. Por fim, concatenamos as três matrizes e resumimos 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 do Python da CCF. A função concatena sucessivamente o resultado da iteração anterior com um novo elemento de proof, resume a concatenação e repete a etapa para cada elemento em proof com o resumo calculado anteriormente. 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 pela 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 da 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 recibos é 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 CCF check_endorsements para validar que a identidade do serviço endossa o nó de assinatura. A cadeia de certificados pode ser composta por certificados de serviço anteriores e, 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 podemos 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 recibos

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óximas etapas