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
:
writeSetDigest
- SHA-256 resumo de
commitEvidence
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
right
ou ).
- Se a chave do elemento atual em
proof
forleft
, o resultado da iteração anterior deverá ser acrescentado ao valor do elemento atual. - Se a chave do elemento atual em
proof
forright
, 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:
- Decodifice a cadeia de caracteres
signature
base64 em uma matriz de bytes. - Extraia a chave pública ECDSA do certificado
cert
do nó de assinatura. - 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:
- Verificação de recibo
- Glossário CCF
- Árvore Merkle
- Criptografia
- Certificados
- Reclamações de aplicação
- Declarações definidas pelo usuário em recibos
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()