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
:
writeSetDigest
- Hash SHA-256 de
commitEvidence
- 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
forleft
, o resultado da iteração anterior deverá ser anexado ao valor do elemento atual. - Se a chave do elemento atual em
proof
forright
, 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:
- Decodificar a cadeia de caracteres base64
signature
em uma matriz de bytes. - Extraia a chave pública ECDSA do certificado do nó de assinatura
cert
. - 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:
- Verificação de recibos
- Glossário do CCF
- Árvore Merkle
- Criptografia
- Certificados
- Declarações do aplicativo
- Declarações definidas pelo usuário em recibos
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.
- A biblioteca do Python da CCF: o módulo fornece um conjunto de ferramentas para verificação de recibos.
- Biblioteca de criptografia do Python: uma biblioteca amplamente usada que inclui vários algoritmos criptográficos e primitivos.
- 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 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()