ステップ 7: 台帳内のドキュメントを検証する - Amazon Quantum 台帳データベース (Amazon QLDB)

翻訳は機械翻訳により提供されています。提供された翻訳内容と英語版の間で齟齬、不一致または矛盾がある場合、英語版が優先します。

ステップ 7: 台帳内のドキュメントを検証する

重要

サポート終了通知: 既存のお客様は、07/31/2025 のサポート終了QLDBまで Amazon を使用できます。詳細については、「Amazon Ledger QLDB を Amazon Aurora Postgre に移行するSQL」を参照してください。

Amazon ではQLDB、SHA-256 で暗号化ハッシュを使用することで、台帳のジャーナル内のドキュメントの整合性を効率的に検証できます。での検証と暗号化ハッシュの仕組みの詳細についてはQLDB、「」を参照してくださいAmazon でのデータ検証 QLDB

このステップでは、vehicle-registration 台帳の VehicleRegistration テーブルのドキュメントリビジョンを確認します。まず、ダイジェストをリクエストします。ダイジェストは出力ファイルとして返され、台帳の変更履歴全体の署名として機能します。次に、そのダイジェストに関連するリビジョンの証明をリクエストします。この証明を使用して、すべての検証チェックに合格すると、リビジョンの整合性が検証されます。

ドキュメントのリビジョンを検証するには
  1. 検証に必要なQLDBオブジェクトが含まれている次の.tsファイルを確認します。

    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. 2 つの .ts プログラム (GetDigest.ts および GetRevision.ts) を使用して、次の手順を実行します。

    • vehicle-registration 台帳に新しいダイジェストをリクエストします。

    • VehicleRegistration テーブルVIN1N4AL11D75C109151から を使用して、ドキュメントの各リビジョンの証明をリクエストします。

    • 返されたダイジェストと証明を使用して、ダイジェストを再計算することで、リビジョンを検証します。

    GetDigest.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 { 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(); }
    注記

    getDigest 関数を使用して、台帳のジャーナルの現在のティップを含むダイジェストをリクエストします。ジャーナルのティップは、 がリクエストQLDBを受信したときの最新のコミット済みブロックを参照します。

    GetRevision.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 { 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(); }
    注記

    getRevision 関数が指定されたドキュメントリビジョンの証明を返した後、このプログラムはクライアント側APIを使用してそのリビジョンを検証します。

  3. トランスパイルされたプログラムを実行するには、次のコマンドを入力します。

    node dist/GetRevision.js

vehicle-registration 台帳を使用する必要がなくなった場合は、「ステップ 8 (オプション): リソースをクリーンアップする」に進みます。