Paso 7: verificar un documento en un libro mayor - Amazon Quantum Ledger Database (Amazon QLDB)

Las traducciones son generadas a través de traducción automática. En caso de conflicto entre la traducción y la version original de inglés, prevalecerá la version en inglés.

Paso 7: verificar un documento en un libro mayor

Con Amazon QLDB, puede verificar de manera eficiente la integridad de un documento del diario de su libro mayor mediante el uso de hash criptográfico con SHA-256. Para obtener más información sobre cómo funcionan la verificación y el hash criptográfico en QLDB, consulte Verificación de datos en Amazon QLDB.

En este paso, verificará la revisión de un documento en la tabla VehicleRegistration del libro mayor vehicle-registration. En primer lugar, solicita un resumen, que se devuelve como un archivo de salida y actúa como firma de todo el historial de cambios del libro mayor. A continuación, solicita una prueba de la revisión relativa a ese resumen. Con esta prueba, se verifica la integridad de la revisión si se aprueban todas las comprobaciones de validación.

Para verificar la revisión de un documento
  1. Revise los siguientes archivos .ts, que contienen los objetos QLDB necesarios para la verificación.

    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. Utilice dos programas .ts (GetDigest.ts y GetRevision.ts) para realizar los siguientes pasos:

    • Solicite un nuevo resumen del libro mayor vehicle-registration.

    • Solicite una prueba de cada revisión del documento con el VIN 1N4AL11D75C109151 de la tabla VehicleRegistration.

    • Verifique las revisiones utilizando el resumen devuelto y compruébelo recalculando el resumen.

    El programa GetDigest.ts contiene el siguiente código.

    /* * 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

    Utilice la función getDigest para solicitar un resumen que incluya el tip actual del diario del libro mayor. La sugerencia del diario hace referencia al último bloqueo comprometido en el momento en que QLDB recibe su solicitud.

    El programa GetRevision.ts contiene el siguiente código.

    /* * 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

    Una vez que la función getRevision devuelve una prueba de la revisión del documento especificado, este programa utiliza una API del lado del cliente para verificar esa revisión.

  3. Para ejecutar el programa transpilado, introduzca el siguiente comando.

    node dist/GetRevision.js

Si ya no necesita usar el libro mayor vehicle-registration, continúe con Paso 8 (opcional): limpiar recursos.