Etapa 7: verificar um documento em um ledger - Amazon Quantum Ledger Database (Amazon QLDB)

As traduções são geradas por tradução automática. Em caso de conflito entre o conteúdo da tradução e da versão original em inglês, a versão em inglês prevalecerá.

Etapa 7: verificar um documento em um ledger

Com o Amazon QLDB, você pode verificar com eficiência a integridade de um documento no diário do seu ledger usando hashing criptográfico com SHA-256. Para saber mais sobre como a verificação e o hashing criptográfico funcionam no QLDB, consulte Verificação de dados no Amazon QLDB.

Nesta etapa, você verifica uma revisão do documento na tabela VehicleRegistration do seu ledger vehicle-registration. Primeiro, você solicita um resumo, que é retornado como um arquivo de saída e atua como uma assinatura de todo o histórico de alterações do seu ledger. Em seguida, você solicita uma prova da revisão em relação a esse resumo. Usando essa prova, a integridade da sua revisão é verificada se todas as verificações de validação forem aprovadas.

Para verificar a revisão de um documento
  1. Analise os arquivos .ts a seguir, que contêm os objetos QLDB necessários para verificação.

    1. BlockAddress.ts

      /* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { ValueHolder } from "aws-sdk/clients/qldb"; import { dom, IonTypes } from "ion-js"; export class BlockAddress { _strandId: string; _sequenceNo: number; constructor(strandId: string, sequenceNo: number) { this._strandId = strandId; this._sequenceNo = sequenceNo; } } /** * Convert a block address from an Ion value into a ValueHolder. * Shape of the ValueHolder must be: {'IonText': "{strandId: <"strandId">, sequenceNo: <sequenceNo>}"} * @param value The Ion value that contains the block address values to convert. * @returns The ValueHolder that contains the strandId and sequenceNo. */ export function blockAddressToValueHolder(value: dom.Value): ValueHolder { const blockAddressValue : dom.Value = getBlockAddressValue(value); const strandId: string = getStrandId(blockAddressValue); const sequenceNo: number = getSequenceNo(blockAddressValue); const valueHolder: string = `{strandId: "${strandId}", sequenceNo: ${sequenceNo}}`; const blockAddress: ValueHolder = {IonText: valueHolder}; return blockAddress; } /** * Helper method that to get the Metadata ID. * @param value The Ion value. * @returns The Metadata ID. */ export function getMetadataId(value: dom.Value): string { const metaDataId: dom.Value = value.get("id"); if (metaDataId === null) { throw new Error(`Expected field name id, but not found.`); } return metaDataId.stringValue(); } /** * Helper method to get the Sequence No. * @param value The Ion value. * @returns The Sequence No. */ export function getSequenceNo(value : dom.Value): number { const sequenceNo: dom.Value = value.get("sequenceNo"); if (sequenceNo === null) { throw new Error(`Expected field name sequenceNo, but not found.`); } return sequenceNo.numberValue(); } /** * Helper method to get the Strand ID. * @param value The Ion value. * @returns The Strand ID. */ export function getStrandId(value: dom.Value): string { const strandId: dom.Value = value.get("strandId"); if (strandId === null) { throw new Error(`Expected field name strandId, but not found.`); } return strandId.stringValue(); } export function getBlockAddressValue(value: dom.Value) : dom.Value { const type = value.getType(); if (type !== IonTypes.STRUCT) { throw new Error(`Unexpected format: expected struct, but got IonType: ${type.name}`); } const blockAddress: dom.Value = value.get("blockAddress"); if (blockAddress == null) { throw new Error(`Expected field name blockAddress, but not found.`); } return blockAddress; }
    2. Verifier.ts

      /* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { Digest, ValueHolder } from "aws-sdk/clients/qldb"; import { createHash } from "crypto"; import { dom, toBase64 } from "ion-js"; import { getBlobValue } from "./Util"; const HASH_LENGTH: number = 32; const UPPER_BOUND: number = 8; /** * Build the candidate digest representing the entire ledger from the Proof hashes. * @param proof The Proof object. * @param leafHash The revision hash to pair with the first hash in the Proof hashes list. * @returns The calculated root hash. */ function buildCandidateDigest(proof: ValueHolder, leafHash: Uint8Array): Uint8Array { const parsedProof: Uint8Array[] = parseProof(proof); const rootHash: Uint8Array = calculateRootHashFromInternalHash(parsedProof, leafHash); return rootHash; } /** * Combine the internal hashes and the leaf hash until only one root hash remains. * @param internalHashes An array of hash values. * @param leafHash The revision hash to pair with the first hash in the Proof hashes list. * @returns The root hash constructed by combining internal hashes. */ function calculateRootHashFromInternalHash(internalHashes: Uint8Array[], leafHash: Uint8Array): Uint8Array { const rootHash: Uint8Array = internalHashes.reduce(joinHashesPairwise, leafHash); return rootHash; } /** * Compare two hash values by converting each Uint8Array byte, which is unsigned by default, * into a signed byte, assuming they are little endian. * @param hash1 The hash value to compare. * @param hash2 The hash value to compare. * @returns Zero if the hash values are equal, otherwise return the difference of the first pair of non-matching bytes. */ function compareHashValues(hash1: Uint8Array, hash2: Uint8Array): number { if (hash1.length !== HASH_LENGTH || hash2.length !== HASH_LENGTH) { throw new Error("Invalid hash."); } for (let i = hash1.length-1; i >= 0; i--) { const difference: number = (hash1[i]<<24 >>24) - (hash2[i]<<24 >>24); if (difference !== 0) { return difference; } } return 0; } /** * Helper method that concatenates two Uint8Array. * @param arrays List of array to concatenate, in the order provided. * @returns The concatenated array. */ function concatenate(...arrays: Uint8Array[]): Uint8Array { let totalLength = 0; for (const arr of arrays) { totalLength += arr.length; } const result = new Uint8Array(totalLength); let offset = 0; for (const arr of arrays) { result.set(arr, offset); offset += arr.length; } return result; } /** * Flip a single random bit in the given hash value. * This method is intended to be used for purpose of demonstrating the QLDB verification features only. * @param original The hash value to alter. * @returns The altered hash with a single random bit changed. */ export function flipRandomBit(original: any): Uint8Array { if (original.length === 0) { throw new Error("Array cannot be empty!"); } const bytePos: number = Math.floor(Math.random() * original.length); const bitShift: number = Math.floor(Math.random() * UPPER_BOUND); const alteredHash: Uint8Array = original; alteredHash[bytePos] = alteredHash[bytePos] ^ (1 << bitShift); return alteredHash; } /** * Take two hash values, sort them, concatenate them, and generate a new hash value from the concatenated values. * @param h1 Byte array containing one of the hashes to compare. * @param h2 Byte array containing one of the hashes to compare. * @returns The concatenated array of hashes. */ export function joinHashesPairwise(h1: Uint8Array, h2: Uint8Array): Uint8Array { if (h1.length === 0) { return h2; } if (h2.length === 0) { return h1; } let concat: Uint8Array; if (compareHashValues(h1, h2) < 0) { concat = concatenate(h1, h2); } else { concat = concatenate(h2, h1); } const hash = createHash('sha256'); hash.update(concat); const newDigest: Uint8Array = hash.digest(); return newDigest; } /** * Parse the Block object returned by QLDB and retrieve block hash. * @param valueHolder A structure containing an Ion string value. * @returns The block hash. */ export function parseBlock(valueHolder: ValueHolder): Uint8Array { const block: dom.Value = dom.load(valueHolder.IonText); const blockHash: Uint8Array = getBlobValue(block, "blockHash"); return blockHash; } /** * Parse the Proof object returned by QLDB into an iterator. * The Proof object returned by QLDB is a dictionary like the following: * {'IonText': '[{{<hash>}},{{<hash>}}]'} * @param valueHolder A structure containing an Ion string value. * @returns A list of hash values. */ function parseProof(valueHolder: ValueHolder): Uint8Array[] { const proofs : dom.Value = dom.load(valueHolder.IonText); return proofs.elements().map(proof => proof.uInt8ArrayValue()); } /** * Verify document revision against the provided digest. * @param documentHash The SHA-256 value representing the document revision to be verified. * @param digest The SHA-256 hash value representing the ledger digest. * @param proof The Proof object retrieved from GetRevision.getRevision. * @returns If the document revision verifies against the ledger digest. */ export function verifyDocument(documentHash: Uint8Array, digest: Digest, proof: ValueHolder): boolean { const candidateDigest = buildCandidateDigest(proof, documentHash); return (toBase64(<Uint8Array> digest) === toBase64(candidateDigest)); }
  2. Use dois .ts programas (GetDigest.ts e GetRevision.ts) para realizar as seguintes etapas:

    • Solicite um novo resumo do ledger vehicle-registration.

    • Solicite uma prova para cada revisão do documento com VIN 1N4AL11D75C109151 da tabela VehicleRegistration.

    • Verifique as revisões usando o resumo e a prova retornados, recalculando o resumo.

    O programa GetDigest.ts contém o código a seguir.

    /* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { QLDB } from "aws-sdk"; import { GetDigestRequest, GetDigestResponse } from "aws-sdk/clients/qldb"; import { LEDGER_NAME } from "./qldb/Constants"; import { error, log } from "./qldb/LogUtil"; import { digestResponseToString } from "./qldb/Util"; /** * Get the digest of a ledger's journal. * @param ledgerName Name of the ledger to operate on. * @param qldbClient The QLDB control plane client to use. * @returns Promise which fulfills with a GetDigestResponse. */ export async function getDigestResult(ledgerName: string, qldbClient: QLDB): Promise<GetDigestResponse> { const request: GetDigestRequest = { Name: ledgerName }; const result: GetDigestResponse = await qldbClient.getDigest(request).promise(); return result; } /** * This is an example for retrieving the digest of a particular ledger. * @returns Promise which fulfills with void. */ const main = async function(): Promise<void> { try { const qldbClient: QLDB = new QLDB(); log(`Retrieving the current digest for ledger: ${LEDGER_NAME}.`); const digest: GetDigestResponse = await getDigestResult(LEDGER_NAME, qldbClient); log(`Success. Ledger digest: \n${digestResponseToString(digest)}.`); } catch (e) { error(`Unable to get a ledger digest: ${e}`); } } if (require.main === module) { main(); }
    nota

    Use a função getDigest para solicitar um resumo que cubra a ponta atual do diário em seu ledger. A ponta do diário se refere ao último bloco confirmado no momento em que o QLDB recebe sua solicitação.

    O programa GetRevision.ts contém o código a seguir.

    /* * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * SPDX-License-Identifier: MIT-0 * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs"; import { QLDB } from "aws-sdk"; import { Digest, GetDigestResponse, GetRevisionRequest, GetRevisionResponse, ValueHolder } from "aws-sdk/clients/qldb"; import { dom, toBase64 } from "ion-js"; import { getQldbDriver } from "./ConnectToLedger"; import { getDigestResult } from './GetDigest'; import { VEHICLE_REGISTRATION } from "./model/SampleData" import { blockAddressToValueHolder, getMetadataId } from './qldb/BlockAddress'; import { LEDGER_NAME } from './qldb/Constants'; import { error, log } from "./qldb/LogUtil"; import { getBlobValue, valueHolderToString } from "./qldb/Util"; import { flipRandomBit, verifyDocument } from "./qldb/Verifier"; /** * Get the revision data object for a specified document ID and block address. * Also returns a proof of the specified revision for verification. * @param ledgerName Name of the ledger containing the document to query. * @param documentId Unique ID for the document to be verified, contained in the committed view of the document. * @param blockAddress The location of the block to request. * @param digestTipAddress The latest block location covered by the digest. * @param qldbClient The QLDB control plane client to use. * @returns Promise which fulfills with a GetRevisionResponse. */ async function getRevision( ledgerName: string, documentId: string, blockAddress: ValueHolder, digestTipAddress: ValueHolder, qldbClient: QLDB ): Promise<GetRevisionResponse> { const request: GetRevisionRequest = { Name: ledgerName, BlockAddress: blockAddress, DocumentId: documentId, DigestTipAddress: digestTipAddress }; const result: GetRevisionResponse = await qldbClient.getRevision(request).promise(); return result; } /** * Query the table metadata for a particular vehicle for verification. * @param txn The {@linkcode TransactionExecutor} for lambda execute. * @param vin VIN to query the table metadata of a specific registration with. * @returns Promise which fulfills with a list of Ion values that contains the results of the query. */ export async function lookupRegistrationForVin(txn: TransactionExecutor, vin: string): Promise<dom.Value[]> { log(`Querying the 'VehicleRegistration' table for VIN: ${vin}...`); let resultList: dom.Value[]; const query: string = "SELECT blockAddress, metadata.id FROM _ql_committed_VehicleRegistration WHERE data.VIN = ?"; await txn.execute(query, vin).then(function(result) { resultList = result.getResultList(); }); return resultList; } /** * Verify each version of the registration for the given VIN. * @param txn The {@linkcode TransactionExecutor} for lambda execute. * @param ledgerName The ledger to get the digest from. * @param vin VIN to query the revision history of a specific registration with. * @param qldbClient The QLDB control plane client to use. * @returns Promise which fulfills with void. * @throws Error: When verification fails. */ export async function verifyRegistration( txn: TransactionExecutor, ledgerName: string, vin: string, qldbClient: QLDB ): Promise<void> { log(`Let's verify the registration with VIN = ${vin}, in ledger = ${ledgerName}.`); const digest: GetDigestResponse = await getDigestResult(ledgerName, qldbClient); const digestBytes: Digest = digest.Digest; const digestTipAddress: ValueHolder = digest.DigestTipAddress; log( `Got a ledger digest: digest tip address = \n${valueHolderToString(digestTipAddress)}, digest = \n${toBase64(<Uint8Array> digestBytes)}.` ); log(`Querying the registration with VIN = ${vin} to verify each version of the registration...`); const resultList: dom.Value[] = await lookupRegistrationForVin(txn, vin); log("Getting a proof for the document."); for (const result of resultList) { const blockAddress: ValueHolder = blockAddressToValueHolder(result); const documentId: string = getMetadataId(result); const revisionResponse: GetRevisionResponse = await getRevision( ledgerName, documentId, blockAddress, digestTipAddress, qldbClient ); const revision: dom.Value = dom.load(revisionResponse.Revision.IonText); const documentHash: Uint8Array = getBlobValue(revision, "hash"); const proof: ValueHolder = revisionResponse.Proof; log(`Got back a proof: ${valueHolderToString(proof)}.`); let verified: boolean = verifyDocument(documentHash, digestBytes, proof); if (!verified) { throw new Error("Document revision is not verified."); } else { log("Success! The document is verified."); } const alteredDocumentHash: Uint8Array = flipRandomBit(documentHash); log( `Flipping one bit in the document's hash and assert that the document is NOT verified. The altered document hash is: ${toBase64(alteredDocumentHash)}` ); verified = verifyDocument(alteredDocumentHash, digestBytes, proof); if (verified) { throw new Error("Expected altered document hash to not be verified against digest."); } else { log("Success! As expected flipping a bit in the document hash causes verification to fail."); } log(`Finished verifying the registration with VIN = ${vin} in ledger = ${ledgerName}.`); } } /** * Verify the integrity of a document revision in a QLDB ledger. * @returns Promise which fulfills with void. */ const main = async function(): Promise<void> { try { const qldbClient: QLDB = new QLDB(); const qldbDriver: QldbDriver = getQldbDriver(); const registration = VEHICLE_REGISTRATION[0]; const vin: string = registration.VIN; await qldbDriver.executeLambda(async (txn: TransactionExecutor) => { await verifyRegistration(txn, LEDGER_NAME, vin, qldbClient); }); } catch (e) { error(`Unable to verify revision: ${e}`); } } if (require.main === module) { main(); }
    nota

    Depois que a função getRevision retorna uma prova da revisão do documento especificada, esse programa usa uma API do lado do cliente para verificar essa revisão.

  3. Para executar o programa transpilado, digite o comando a seguir.

    node dist/GetRevision.js

Se você não precisar mais usar o vehicle-registration ledger, prossiga para Etapa 8 (opcional): Limpar os recursos.