O blog da AWS

Verificação criptográfica com Amazon QLDB no mundo real

Por Dan Blaner, Sr. Amazon QLDB Specialist Solutions Architect

O Amazon Quantum Ledger Database (Amazon QLDB) é um banco de dados livro-razão que fornece um histórico completo e verificável de todas as transações confirmadas no livro-razão, ou ledger.

No Amazon QLDB, registros são chamados documentos e são versionados. Documentos usam um formato similar ao formato JSON chamado Amazon Ion. Alterações em documentos são chamadas revisões e são gravadas no ledger em transações. O ledger é construído sobre um log append-only, ou seja, que apenas aceita a inclusão de dados, chamado journal. À medida que transações são confirmadas no ledger, elas são organizadas em blocos e acrescentadas ao jornal, ou journal, com cada bloco contendo uma transação. Uma vez confirmados e adicionados, os blocos não podem ser modificados ou sobrescritos, o que permite a entrega de um registro imutável de cada operação insert, update, delete e select já confirmadas no ledger e também o acesso a cada revisão de cada documento. Para comprovar que o histórico de transações é imutável, o Amazon QLDB fornece uma funcionalidade de verificação criptográfica que permite que se comprove matematicamente a integridade do histórico de transações. Nesse post, discutimos o valor da verificação criptográfica em um ledger no contexto de um caso de uso real.

Caso de uso de verificação criptográfica

A verificação criptográfica é útil em casos nos quais o negócio pode precisar fornecer transparência provando a integridade dos seus dados para um terceiro como regulador, agente da lei, auditores, o público, um parceiro de negócios, ou um terceiro em processo judicial. A verificação prova que o histórico de transações não foi alterado, adulterado ou falsificado. A console do Amazon QLDB fornece uma funcionalidade de fácil utilização para verificar uma única revisão de um documento. Porém, usar a console para verificação é um processo manual e verifica apenas uma única revisão de um documento. Você pode querer automatizar verificações ou integrar a funcionalidade de verificação à sua aplicação.

Para ilustrar, vamos imaginar que estamos em uma produtora de televisão hospedando uma competição de canto sendo televisionada ao vivo na qual os competidores avançam ou são eliminados baseado em votos submetidos pelos telespectadores usando um aplicativo de celular. A confiabilidade de programas de TV é levada a sério e a maioria das produtoras usa uma empresa de auditoria para revisar seu processo de votação e o resultado. Contudo, seria fácil acusar produtores de adulterar resultados em favor de competidores que possam aumentar a audiência do programa. A adulteração de dados diretamente no banco de dados seria difícil para os auditores detectarem, então ainda existe espaço para um escândalo. Se os votos estiverem armazenados em um ledger do Amazon QLDB, eles possuirão um registro completo de cada transação já confirmada, então não haveria como esconder uma suposta manipulação da competição e a integridade dos dados poderá ser comprovada com criptografia.

Nesse post, vamos ver como a verificação criptográfica pode ajudar sua produtora manter a confiança da sua audiência no processo de votação e seus resultados. Antes de aprofundarmos no nosso caso de uso de exemplo, vamos entender os conceitos por trás da verificação criptográfica.

Hash criptográfico

Enviar dados através de um algoritmo criptográfico de hash produz uma saída de tamanho fixo chamado hash. Um hash criptográfico é como uma impressão digital do dado. Se um único bit é alterado no dado de entrada o hash é recalculado e o algoritmo produz um hash bem diferente. Ao comparar os hashes de um dado calculado em diferentes pontos no tempo, é fácil dizer se o dado foi alterado durante esse período: os hashes são diferentes! Quando o dado é gravado no ledger, o Amazon QLDB calcula o hash do dado usando o algoritmo de hash SHA-256 e armazena junto com o dado. Em alto nível, a verificação de dados no Amazon QLDB é feita recalculando o hash para o dado que você quer verificar e comparando com o hash que foi armazenado quando o dado foi gravado.

Vamos examinar um simples exemplo de comparação de hash usando Java. Nesse exemplo, nós calculamos o hash SHA-256 de dados de exemplo. Então nós simulamos a adulteração dos dados ao alterar um caractere neles. Para realizar a verificação, nós recalculamos o hash SHA-256 dos nossos dados e o comparamos com nosso hash original. Se eles forem os mesmos, provamos a integridade do nosso dado. Se eles forem diferentes, saberemos que o dado original foi alterado. Veja o código seguinte:

public static void main (String... args) throws Exception {

    MessageDigest md = MessageDigest.getInstance("SHA-256");

    String str1 = "Lorem ipsum dolor sit amet, consectetur adipiscing elit";
    byte[] digest1 = md.digest(str1.getBytes(StandardCharsets.UTF_8));
    System.out.println(Base64.getEncoder().encodeToString(digest1));

    md.reset();

    String str2 = str1.replaceFirst("L", "l");
    byte[] digest2 = md.digest(str2.getBytes(StandardCharsets.UTF_8));
    System.out.println(Base64.getEncoder().encodeToString(digest2));

    System.out.println(Arrays.equals(digest1, digest2) ? "Data is valid" : "Data has been tampered with!");
}

Na saída para esse programa os hashes são diferentes, indicando que o dado foi alterado:

B/5NSiVxgkGvFFqT+JDrVGkFLiUdGZ0XO9O9UMO7TaI=
UMWgxQc+VLxvWVkaTuaDieJ8V5jLatZOrMnab2qD2XM=
O dado foi adulterado!

Cadeia de Hash

E se quiséssemos verificar a integridade de um banco de dados inteiro? Poderíamos aplicar a técnica de hash criptográfico que descrevemos em cada registro do banco de dados, testando a integridade de cada um, como mostrado no diagrama a seguinte.

Contudo, essa abordagem possui algumas falhas. Em primeiro lugar, uma pessoa mal intencionada com acesso de baixo nível poderia adulterar os dados e seu hash e seria difícil descobrir essa adulteração. Em segundo lugar, essa abordagem verifica cada registro individualmente, não a integridade do banco de dados como um todo.

Para resolver esses problemas, usamos o encadeamento de hash. O encadeamento de hash é o processo de aplicar o algoritmo de hash criptográfico a um outro hash, criando o hash de um hash. Se organizarmos os registros no nosso banco de dados sequencialmente e fizermos com que o hash de cada registro dependa do hash do registro anterior a ele, então a alteração dos dados em um registro afeta o hash daquele registro e também os hashes de cada outro registro que ocorre depois dele no banco de dados. Para verificar a integridade do banco de dados como um todo, nós recalculamos e comparamos os hashes de todos os registros, começando pelo primeiro registro e continuando até que o hash do último registro seja verificado ou até descobrirmos um hash inválido. Isso comprova não apenas o conteúdo de cada registro, mas também que cada registro está onde deveria na ordem sequencial do banco de dados. O diagrama seguinte ilustra nosso processo de encadeamento de hash.

Árvore Merkle

Recalcular a cadeia de hashes inteira durante uma verificação é ineficiente e requer cada vez mais tempo e ciclos computacionais à medida que o journal cresce. Para tornar verificações mais eficientes, nós podemos organizar nossos hashes usando uma árvore Merkle. Uma árvore Merkle é uma estrutura de dados de árvore binária na qual os nós do tipo folha contém um único hash de dados e os nós não-folha contém o hash dos seus dois hashes filhos.

Verificações usando uma árvore Merkle envolvem recomputar o hash do dado que você deseja verificar, concatenando esse hash com o hash do seu nó irmão na árvore Merkle, e gerando o hash do resultado. Esse hash se torna o valor do nó pai. O hash do nó pai então é combinado com o hash contido no seu nó irmão na árvore Merkle, e o resultado tem um hash gerado para produzir o hash do nó avô. Esse processo continua até que os dois nós abaixo do nó raiz da árvore Merkle sejam combinados e tenham seu hash gerado em conjunto. O hash resultante deve ser igual ao hash contido no nó raiz. Verificações executadas usando árvores Merkle são muito mais eficientes do que executar uma validação linear de cada registro do banco de dados.

Como cada nó da árvore Merkle é um hash que representa tudo abaixo dele na árvore, a raiz da árvore Merkle representa o banco de dados inteiro até aquele momento no tempo. Verificar a integridade dos dados usando a árvore Merkle comprova a integridade daquele dado e do banco de dados como um todo.

O diagrama a seguir ilustra nossa árvore Merkle.

Vamos imaginar que queremos verificar o registro d2 no diagrama anterior. O banco de dados no diagrama contém seis registros, identificados de d0 a d5. Cada registro tem um valor de hash correspondente, identificados de h0 a h5. Começamos calculando o hash do dado em d2. Existe uma forma específica para fazer isso; vamos cobrir os detalhes mais a frente no post. Nós combinamos o hash que computamos para o dado d2 com o hash h1 e geramos o hash dessa combinação, cujo resultado deve ser igual a h2, e que o Amazon QLDB computou e armazenou quando d2 foi gravado.

Agora continuamos subindo nos níveis da árvore Merkle. O próximo passo é combinar os hashes irmãos h2 e h3 e gerando o hash da combinação, nos dando m0.2. Repetimos o processo, combinando os hashes irmãos m0.2 e m0.1, nos dando m1.0. Repetimos o processo mais uma vez, concatenando os hashes m1.0 e m0.3 e gerando o hash da combinação. Se o hash resultante for igual ao hash m2.0, nosso banco de dados está válido. Entretanto, se um único bit for alterado nos registros d0 até d5, a verificação falhará.

Uma complicação de usar a árvore Merkle para realizar validações é que você simplesmente não tem algumas informações a não ser que você tenha construído a árvore Merkle inteiramente por conta própria, o que derruba seu propósito. Especificamente, você não tem os hashes irmãos em cada nível da árvore e você não tem o hash final, o hash da raiz da árvore Merkle, para fazer a comparação. Felizmente, o Amazon QLDB os fornece para você. O Amazon QLDB chama os hashes irmãos de hashes de prova e chama o hash na raiz da árvore Merkle de digest. No nosso exemplo de verificação, os hashes h3m0.1, e m0.3 são os hashes de prova fornecidos pelo Amazon QLDB nessa ordem. Eles estão na cor verde no diagrama a seguir. O hash m2.0 é o digest do ledger, na cor azul.

Os nós de dados na cor cinza no diagrama são chamados blocos no Amazon QLDB, e a coleção ordenada de blocos é chamada journal. Cada bloco representa uma única transação confirmada no ledger, e é uma estrutura complexa que contém todas as revisões de documentos gravados na transação, metadados da transação e outras informações. Para calcular o hash do bloco, aplicamos o conceito da árvore Merkle ao próprio bloco. Os hashes dos componentes individuais de um bloco são calculados e organizados em uma árvore Merkle para que possamos calcular o hash da raiz, que se torna o hash do nosso bloco. Nós demonstramos isso nas sessões seguintes.

Agora que entendemos os conceitos por trás da verificação criptográfica, vamos demonstrar como podemos verificar os votos da competição de canto.

Verificação de votos

Toda vez que alguém submete um voto para seu cantor favorito no nosso programa de televisão, nós armazenamos um documento na tabela votes do nosso ledger contest. O documento contém um ID de voto único, o número de telefone do votante, o episódio do programa, o competidor que recebeu o voto, e outras informações sobre o votante e o seu dispositivo. A tabela possui índices nos campos voteId phone para busca eficiente. O documento parece com o seguinte:

{
	'voteId': '19374394835',
	'phone': '7035551212',
	'episode': 46,
	'candidate': 'Nikki Wolf',
	'method': 'mobile app',
	'device': 'Acme Phone',
	'os': 'acme-os 3.1.4',
	'location': {
		'lat': '38.9547917',
		'lon': '-77.4065622,17'
	}
}

Digamos que os membros da audiência podem ver o histórico dos seus votos para uma temporada inteira do programa, e que a pessoa que submeteu o voto anterior acredita que tenha votado em um outro competidor. Eles reclamam que seus votos foram alterados e vão para as mídias sociais dizer ao mundo que a competição é manipulada. Como parte da nossa investigação do assunto, nós realizamos a verificação criptográfica do documento do voto para provar que ele permanece exatamente como havia sido gravado inicialmente.

Verificação do hash da revisão do documento

Para realizar a verificação, precisamos de várias coisas. Primeiramente, precisamos de uma cópia da revisão do documento do seu voto e seus metadados do ledger. Os metadados contém informações importantes como o endereço do bloco onde o documento reside no ledger assim como o hash criptográfico para a revisão do documento que o Amazon QLDB calculou e armazenou quando a revisão foi gravada. Para buscar a nossa revisão do documento e seus metadados, nos consultamos a committed view, ou visualização confirmada, da tabela votes com a seguinte consulta:

select * from _ql_committed_votes where data.voteId = '19374394835'

O documento que recebemos parece com o seguinte:

{
  blockAddress:{
    strandId:"Chnkm8xnPV3BndOuZ2zMqy",
    sequenceNo:71
  },
  hash:{{ JFRbF/JEw5yXvh6jpvawqxKpiLUM5fstC/HvXrTgHYM= }},
  data:{
    voteId:"19374394835",
    phone:"7035551212",
    episode:46,
    candidate:"Nikki Wolf",
    method:"mobile app",
    device:"Acme Phone",
    os:"acme-os 3.1.4",
    location:{
      lat:"38.9547917",
      lon:"-77.4065622,17"
    }
  },
  metadata:{
    id:"0luLGw4fg9q3stlfnjGc54",
    version:0,
    txTime:2021-08-10T15:55:02.663Z,
    txId:"BVHPUtDjjmsEq2PEOBlP98"
  }
}

Como nós consultamos a committed view, nós recebemos um documento onde nossos dados do voto estão agora aninhados sob o campo data. É por isso que selecionamos nosso documento usando data.voteId  no lugar de voteId. O documento também contem os campos blockAddresshash, e metadata no nível superior.

Esse documento é nosso ponto de partida no journal para nossa verificação. Começamos verificando o hash do documento. Para fazer isso, nós calculamos os hashes individuais das sessões data e metadata. Então nós os combinamos de uma certa forma e calculamos o hash do resultado. Esse hash deve ser igual ao hash já armazenado com o documento.

O trecho de código Java a seguir mostra o processo como um todo. Note que nosso documento, que contém seus metadados e outros campos, é passado como um objeto IonStruct.

private static byte[] verifyDocumentHash(IonStruct document) throws Exception {
    // Esse é o hash armazenado juntamente aos dados da revisão do documento
    byte[] hash = ((IonBlob) document.get("hash")).getBytes();

    // Gere o hash dos metadados da revisão do documento (não inclui o hash do documento)
    byte[] metadataHash = hashIonValue(document.get("metadata"));

    // Agora gere o hash dos dados do "payload" da revisão do documento
    byte[] dataHash = hashIonValue(document.get("data"));

    // Agora calculamos o "hash candidato" que é criado concatenando os hashes dos metadados e dados de certa forma
    // and data hashes in a certain way.
    byte[] candidateHash = combineHashes(metadataHash, dataHash);
    if (!Arrays.equals(candidateHash, hash)) {
        throw new RuntimeException("Document hash does not correctly re-compute");
    }

    return hash;
}

As partes interessantes ocorrem em hashIonValue() e combineHashes(), que não são mostradas. Vamos aprofundar nelas.

Quando calculamos o hash criptográfico de um dado, é importante fazê-lo da mesma forma toda vez ou você poderá produzir diferentes hashes do mesmo dado. Isso é especialmente verdadeiro quando você estiver lidando um formato similar ao JSON como o Amazon Ion onde a ordenação dos campos não importa e pode mudar. Considere os dois documentos de exemplo a seguir:

{
   “firstName”: “John”,
   “lastName”: “Doe”,
   “age”: 30
}
{
	“lastName”: “Doe”,
	“firstName”: “John”,
	“age”: 30
}

Esses dois documentos são equivalentes do ponto de vista dos dados. Eles contêm os mesmos campos com os mesmos valores. Contudo não tem a mesma ordenação de bytes, então seus hashes serão diferentes. Para garantir que estamos calculando hashes dos nossos documentos do Amazon QLDB formatados em Amazon Ion consistentemente, usamos a biblioteca Ion Hash, que faz o trabalho duro por nós. Nosso método hashIonValue() demonstra como usar a biblioteca para calcular um hash:

private static final IonSystem ionSystem = IonSystemBuilder.standard().build();

private static final MessageDigestIonHasherProvider ionHasherProvider = new MessageDigestIonHasherProvider("SHA-256");private static byte[] hashIonValue(final IonValue ionValue) {
    IonReader reader = ionSystem.newReader(ionValue);
    IonHashReader hashReader = IonHashReaderBuilder.standard()
            .withHasherProvider(ionHasherProvider)
            .withReader(reader)
            .build();
    while (hashReader.next() != null) {  }
    return hashReader.digest();
}

Verificar o hash de uma revisão de um documento requer concatenar os hashes dos componentes data metadata do documento e então calculando o hash do resultado. A concatenação sempre deve ser feita consistentemente para produzir os resultados corretos. Isso é especialmente importante posteriormente quando iremos verificar passo a passo da árvore Merkle, porque os hashes de prova são entregues a nós pelo Amazon QLDB como uma lista, sem nenhuma referência à árvore Merkle. Como vimos anteriormente, alguns hashes de prova podem ser irmãos à direita e alguns podem ser irmãos à esquerda, mas nós não saberemos disso quando estivermos percorrendo nossa árvore. Portanto, precisamos de outros meios de garantir a consistência à medida que combinamos os hashes. Fazemos isso ordenando os hashes usando uma comparação de bits da esquerda para a direita, sempre acrescentando o hash maior ao hash menor. Essa abordagem de combinar hashes é única ao Amazon QLDB. Outros sistemas de prova Merkle requerem a distinção de hashes à direita de hashes à esquerda. Nosso código combineHashes() parece com o seguinte código:

private final static int HASH_LENGTH = 32;

private static final Comparator<byte[]> hashComparator = (h1, h2) -> {
    if (h1.length != HASH_LENGTH || h2.length != HASH_LENGTH) {
        throw new IllegalArgumentException("Invalid hash.");
    }
    for (int i = h1.length - 1; i >= 0; i--) {
        int byteEqual = Byte.compare(h1[i], h2[i]);
        if (byteEqual != 0) {
            return byteEqual;
        }
    }

    return 0;
};private static byte[] combineHashes(final byte[] h1, final byte[] h2) throws Exception {
    if (h1.length == 0) {
        return h2;
    }

    if (h2.length == 0) {
        return h1;
    }

    byte[] combined = new byte[h1.length + h2.length];
    if (hashComparator.compare(h1, h2) < 0) {
        System.arraycopy(h1, 0, combined, 0, h1.length);
        System.arraycopy(h2, 0, combined, h1.length, h2.length);
    } else {
        System.arraycopy(h2, 0, combined, 0, h2.length);
        System.arraycopy(h1, 0, combined, h2.length, h1.length);
    }

    MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
    messageDigest.update(combined);

    return messageDigest.digest();
}

Obtenha um digest

Agora que nós verificamos o hash da revisão do documento, nós podemos verificar a revisão no contexto do ledger. Para fazer isso, precisamos de um digest do ledger e os hashes de prova, os hashes intermediários que nos levam do hash do nosso documento acima na árvore Merkle até o digest. O Amazon QLDB fornece a ação de API GetDigest para nos conseguir o digest. O código Java seguinte busca o digest mais recente do ledger:

private static final QldbClient qldbClient = QldbClient.builder().build();private static GetDigestResponse getDigest(String ledgerName) {
    GetDigestRequest request = GetDigestRequest.builder().name(ledgerName).build();
    return qldbClient.getDigest(request);
}

O digest do ledger é o documento Amazon Ion que contém o hash do digest assim como um ponteiro para a localização no histórico transacional do journal de onde ele foi criado. Esse ponteiro é chamado de endereço da ponta, ou tip address. O que segue é um exemplo de um digest de um ledger:

{
    "Digest": "OhYME7Nfic0XqJUC1glwrrXZ+8kp11rmnfcWJ8w7skc=",
    "DigestTipAddress": {
        "IonText": "{strandId:\"Chnkm8xnPV3BndOuZ2zMqy\",sequenceNo:73}"
    }
}

Obtenha os hashes de prova

Agora nós obtemos os hashes the prova. Os hashes de prova são os hashes intermediários na árvore Merkle entre nossa revisão do documento e o digest, então o Amazon QLDB precisa do endereço do bloco da revisão e o endereço da ponta do digest para que ele saiba quais hashes de prova são necessários. O seguinte código chama a ação de API GetRevision para recuperar os hashes de prova para nossa revisão de documento e o digest que acabemos de obter.

private static final QldbClient qldbClient = QldbClient.builder().build();private static List<byte[]> getDocumentProofs(String ledgerName, IonStruct document, ValueHolder digestTipAddress) {
    List<byte[]> proofs = new ArrayList<>();

    // Extraia alguns dados do nosso documento e armazene em variáveis para conveniência
    String documentId = ((IonString) (((IonStruct) document.get("metadata")).get("id"))).stringValue();
    ValueHolder blockAddress = ValueHolder.builder().ionText(document.get("blockAddress").toString()).build();

    // Agora construa e execute a requisição para buscar as provas
    GetRevisionRequest request = GetRevisionRequest.builder()
            .name(ledgerName)
            .digestTipAddress(digestTipAddress)
            .blockAddress(blockAddress)
            .documentId(documentId)
            .build();

    GetRevisionResponse result = qldbClient.getRevision(request);

    // Converta as provas em um formato que iremos precisar depois
    IonReader reader = ionSystem.newReader(result.proof().ionText());
    reader.next();
    reader.stepIn();
    while (reader.next() != null) {
        proofs.add(reader.newBytes());
    }

    return proofs;
}

Recalcule o digest

Agora que temos todas as partes nós precisamos conduzir uma verificação criptográfica completa de uma revisão do documento. O ultimo passo é calcular o caminho acima na árvore Merkle até o topo e comparar nosso hash calculado com o digest do ledger que o Amazon QLDB nos entregou. O código para fazer isso parece com o seguinte:

private static boolean verify(IonStruct document, List<byte[]> proofs, byte[] digest) throws Exception {

    byte[] candidateDigest = verifyDocumentHash(document);

    for (byte[] proof : proofs) {
        candidateDigest = combineHashes(candidateDigest, proof);
    }

    return Arrays.equals(digest, candidateDigest);
}

Nós começamos com o hash do documento verificado, combinamos ele com o primeiro hash de prova na lista, e geramos o hash do resultado. Pegamos esse hash e combinamos com o próximo hash de prova na lista, gerando o hash do resultado. Continuamos esse processo até que tenhamos esgotado os hashes de prova. O hash final deve ser igual ao hash do digest do ledger. Caso seja, nós verificamos com sucesso a integridade do nosso documento e do ledger até o momento em que o digest foi criado. O seguinte trecho de código junta todas essas partes:

public static void main(String[] args) throws Exception {

    String ledgerName = "contest";

    // Get document
    IonStruct vote = readDocument(ledgerName, "19374394835"); // Implementation not shown

    // Generate a digest
    GetDigestResponse digestResult = getDigest(ledgerName);

    // Get proof hashes
    List<byte[]> proofs = getDocumentProofs(ledgerName, vote, digestResult.digestTipAddress());

    // Now verify!
    if (verify(vote, proofs, digestResult.digest().asByteArray())) {
        System.out.println("Document verified!");
    } else {
        System.out.println("Document NOT verified!");
    }
}

Nós provamos a integridade do voto no membro da audiência e podemos responder à sua reclamação nas mídias sociais. Melhor ainda, nós podemos permitir aos usuários do aplicativo de celular solicitar a verificação de um voto, mostrando um indicador visual do estado da verificação próximo ao seu voto no aplicativo.

Verifique os blocos

Agora que nós entendemos os rudimentos da verificação criptográfica, podemos expandir nossa capacidade de construir confiança em nossos dados. Além de verificar revisões individuais de documentos, podemos também realizar verificações criptográficas em blocos inteiros no journal. A verificação de blocos usa as mesmas técnicas que usamos para verificar a revisão do documento.

Para ilustrar, vamos imaginar que nossa produtora de televisão usa um auditor externo para validar os resultados no nosso concurso de canto televisionado. Dar aos auditores acesso direto ao nosso banco de dados de votos não é ideal. Em vez disso, nós podemos exportar os blocos do nosso journal para o Amazon Simple Storage Service (Amazon S3) usando a funcionalidade de export do Amazon QLDB, empacotá-los, e entregá-los aos auditores como um conjunto de dados ou dataset. Os blocos dão aos auditores não apenas o dado do voto, mas também o contexto transacional e os metadados, e o histórico completo de cada operação de insert, update e delete executada no banco de dados.

Os auditores naturalmente ficarão preocupados com a integridade dos dados na exportação, porque o dado deixou as fronteiras protetivas do banco de dados. Como eles tem os blocos do journal por completo na exportação, eles podem realizar uma verificação criptográfica dos blocos nos arquivos de exportação. A exportação pode ser comprovada diante do próprio ledger usando um digest do ledger e os hashes de prova fornecidos pelo Amazon QLDB.

Uma exportação do Amazon QLDB cria um arquivo de manifesto completo que contém uma lista ordenada de caminhos para os arquivos de dados exportados, cada arquivo de dados contendo um ou mais blocos do journal. Verificar a exportação envolve ler e verificar os blocos em ordem, então verificar o último bloco diante do digest do ledger. Os blocos são encadeados uns com o outros pelos hashes, então o hash de cada bloco é usado para calcular o hash do próximo bloco.

Verificar um bloco é um pouco diferente de verificar uma revisão de um documento. O código a seguir é um exemplo de um bloco do nosso ledgercontest:

{
  blockAddress:{
    strandId:"Chnkm8xnPV3BndOuZ2zMqy",
    sequenceNo:75
  },
  transactionId:"2uyxJjHdyMCHbFLihxCqkI",
  blockTimestamp:2021-08-10T18:43:11.150Z,
  blockHash:{{ fns/0/y73vT2mZfCrEWsunk+47KikEDxJedU3bl1dvw= }},
  entriesHash:{{ AGp8OSLeZRPTkieAohwC8+M7CtzIArrGrsxynTmo2wM= }},
  previousBlockHash:{{ MU7eUIkqfiI1ijx5JUcbk1Y1FyZzIepjWO7JZP97gKU= }},
  entriesHashList:[
    {{ L3ljDI5hySRpMkvNQIPfNNx3d5kPQV6pGyhvQAapdBE= }},
    {{ G+EI/alz7hytZJpt9LAekGcz5PX2AwhdAJQhHJPhBLI= }},
    {{ rNZo95Z4rwh1I2QuoIBluz91RCFNwJlLsUYdecx4BQc= }},
    {{ An14tkUzXU9Qz/Cd3ipu5bF5JZoYaXlCxs8o7K/SWwI= }}
  ],
  transactionInfo:{
    statements:[
      {
        statement:"insert into votes value\n{\n'voteId': '87475627475',\n'phone': '2065551212',\n'episode': 46,\n'candidate': 'John Stiles',\n'method': 'mobile app',\n'device': 'Nifty Tablet',\n'os': 'nifty-os 2.1',\n'location': {\n'lat': '47.6164017',\n'lon': '-122.3343053,19'\n}\n}",
        startTime:2021-08-10T18:43:02.452Z,
        statementDigest:{{ upMaMY2Jpg7Iy+seE6wIkLLTSgphieWdu/UPocvDh4U= }}
      },
      {
        statement:"insert into votes value\n{\n'voteId': '87475627474',\n'phone': '2025551212',\n'episode': 46,\n'candidate': 'Martha Rivera',\n'method': 'mobile app',\n'device': 'Smart Fridge',\n'os': 'brrr-os 1.0',\n'location': {\n'lat': '38.8976763',\n'lon': '-77.0365298'\n}\n}",
        startTime:2021-08-10T18:43:09.894Z,
        statementDigest:{{ 4ow5lahovcXjlGLJ7hMSGf1yg1PTqQJjEg1mG2Ycr1E= }}
      }
    ],
    documents:{
      '9BYkPOlkq067uqoi99WXjT':{
        tableName:"votes",
        tableId:"CX9hjYznkyj4Jojfo7pwdM",
        statements:[
          0
        ]
      },
      KQrxlSirZCGIFKt0WjQfr1:{
        tableName:"votes",
        tableId:"CX9hjYznkyj4Jojfo7pwdM",
        statements:[
          1
        ]
      }
    }
  },
  revisions:[
    {
      blockAddress:{
        strandId:"Chnkm8xnPV3BndOuZ2zMqy",
        sequenceNo:75
      },
      hash:{{ DdxJn3PB0dg26m7VvTDJYOFgURoWeR53b1z/WeM6vLE= }},
      data:{
        voteId:"87475627475",
        phone:"2065551212",
        episode:46,
        candidate:"John Stiles",
        method:"mobile app",
        device:"Nifty Tablet",
        os:"nifty-os 2.1",
        location:{
          lat:"47.6164017",
          lon:"-122.3343053,19"
        }
      },
      metadata:{
        id:"9BYkPOlkq067uqoi99WXjT",
        version:0,
        txTime:2021-08-10T18:43:11.133Z,
        txId:"2uyxJjHdyMCHbFLihxCqkI"
      }
    },
    {
      blockAddress:{
        strandId:"Chnkm8xnPV3BndOuZ2zMqy",
        sequenceNo:75
      },
      hash:{{ 7Fh105cH/dzL116STpr0xfrLs7YcIRfz+gm6Gp2TnKQ= }},
      data:{
        voteId:"87475627474",
        phone:"2025551212",
        episode:46,
        candidate:"Martha Rivera",
        method:"mobile app",
        device:"Smart Fridge",
        os:"brrr-os 1.0",
        location:{
          lat:"38.8976763",
          lon:"-77.0365298"
        }
      },
      metadata:{
        id:"KQrxlSirZCGIFKt0WjQfr1",
        version:0,
        txTime:2021-08-10T18:43:11.133Z,
        txId:"2uyxJjHdyMCHbFLihxCqkI"
      }
    }
  ]
}

O diagrama a seguir ilustra essas partes do bloco. No topo do bloco está o endereço que especifica a localização do bloco no journal. O endereço é seguido do ID da transação e a marcação de tempo, ou timestamp (cada bloco representa uma transação), o hash calculado do bloco, e o hash do bloco anterior. Então nós temos os campos entriesHash entriesHashList, que vamos explicar posteriormente nessa postagem. O próximo é a sessão transactionInfo que contém os comandos da consulta PartiQL que foi executada para criar a transação para esse bloco e um mapeamento das revisões de documento no bloco para as tabelas do banco de dados em que elas residem. A última sessão é a revisions, que contém todas as revisões de documentos nas transações desse bloco. O bloco precedente contém duas revisões de documentos.

Para verificar o bloco, comece com a sessão revisions. Recalcule o hash de cada revisão e o compare com o hash armazenado com aquela revisão, assim como fizemos quando verificamos a revisão anteriormente nessa postagem. Agora, itere pelos hashes das revisões, concatenando o hash da primeira revisão com o hash da próxima revisão e gerando o hash do resultado, então repita até o fim da lista, assim como fizemos com os hashes de prova. Isso produz um hash final para a sessão revisions. Esse hash deve aparecer na lista entriesHash na sessão do topo do bloco. A sua localização na lista não importa.

A seguir, calculamos o hash para a sessão transactionInfo. Nós podemos usar o código hashIonValue() que descrevemos anteriormente no artigo. Por exemplo:

byte[] transactionInfoHash = hashIonValue(block.get("transactionInfo"));

O hash resultante deve aparecer na lista entriesHash na sessão do topo do bloco. A sua localização na lista não importa. A lista contém mais do que os dois hashes que calculamos para as sessões transactionInfo e revisions do nosso bloco. Esses outros hashes representam o estado interno dos componentes do ledger no momento que a transação foi confirmada e não precisam ser recalculados para a verificação.

Então, calculamos o hash para o elemento entriesHashList processando os hashes na lista assim como fizemos para os hashes de prova. O hash resultante deve ser igual ao campo entriesHash no topo do bloco.

À medida que processamos os blocos da nossa exportação em sequência, o valor do campo previousBlockHash no topo do bloco deve ser igual ao campo blockHash do bloco anterior na exportação.

Em seguida, combinamos o campo previousBlockHash com o campo entriesHash e geramos o hash do resultado. Ele deve ser igual do campo blockHash, que deve também ser igual ao campo previousBlockHash do próximo bloco da exportação.

Quando chegamos no último bloco da exportação, podemos verificá-lo diante do digest do ledger. Isso prova que os blocos exportados correspondem aos blocos no banco de dados, dando aos auditores confiança nas nossas exportações para que eles possam conduzir sua auditoria.

O processo de verificar um bloco diante do digest é quase idêntico ao processo que seguimos anteriormente para verificar uma revisão de documento diante do digest. Busque o digest usando o método getDigest() que usamos anteriormente. Para buscar os hashes de prova para nosso bloco, use a ação API GetBlock ao invés de GetRevision. Combine o hash do bloco com o primeiro hash de prova e gere o hash do resultado. Então avance através dos hashes de prova assim como fizemos previamente até que tenhamos um hash final que deve ser igual ao digest.

Resumo

A verificação criptográfica é um mecanismo poderoso para provar a integridade de dados em um ledger do Amazon QLDB. Verificabilidade transmite confiança nos dados, e o uso de um banco de dados versionado com histórico imutável e verificável de mudanças demonstra um compromisso com a transparência.

Nessa postagem, nós demonstramos diversas maneiras de usar verificação criptográfica que são aplicáveis a muitos diferentes casos de uso. A verificação pode ser realizada em cada uma das linguagens de programação suportadas pelo Amazon QLDB. O Guia do Desenvolvedor do Amazon QLDB fornece links para aplicativos de amostra em cada uma dessas linguagens, que contém exemplos de como realizar verificações criptográficas usando aquela linguagem.

Começe agora solicitando um digest e verificando um documento no seu ledger.

Este artigo foi traduzido do Blog da AWS em Inglês.


Sobre o autor

Dan Blaner é Arquiteto de Soluções Senior especializado em Amazon QLDB.

 

 

 

 

Willer Púlis é Arquiteto de Soluções Enterprise para clientes da indústria financeira na AWS. Ele trabalha com tecnologias de banco de dados a mais de 12 anos e adora falar sobre elas. Willer também gosta de cozinhar e de arquitetura (a dos prédios/casas e belos acabamentos).