ステップ 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. Ion 値と文字列値のヘルパーメソッドを使用して、検証クラスとユーティリティクラスに必要なQLDBオブジェクトを表す次の.javaファイルを確認します。

    1. BlockAddress.java

      /* * 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. */ package software.amazon.qldb.tutorial.qldb; import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; /** * Represents the BlockAddress field of a QLDB document. */ public final class BlockAddress { private static final Logger log = LoggerFactory.getLogger(BlockAddress.class); private final String strandId; private final long sequenceNo; @JsonCreator public BlockAddress(@JsonProperty("strandId") final String strandId, @JsonProperty("sequenceNo") final long sequenceNo) { this.strandId = strandId; this.sequenceNo = sequenceNo; } public long getSequenceNo() { return sequenceNo; } public String getStrandId() { return strandId; } @Override public String toString() { return "BlockAddress{" + "strandId='" + strandId + '\'' + ", sequenceNo=" + sequenceNo + '}'; } @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } BlockAddress that = (BlockAddress) o; return sequenceNo == that.sequenceNo && strandId.equals(that.strandId); } @Override public int hashCode() { // CHECKSTYLE:OFF - Disabling as we are generating a hashCode of multiple properties. return Objects.hash(strandId, sequenceNo); // CHECKSTYLE:ON } }
    2. Proof.java

      /* * 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. */ package software.amazon.qldb.tutorial.qldb; import com.amazon.ion.IonReader; import com.amazon.ion.IonSystem; import com.amazon.ion.system.IonSystemBuilder; import com.amazonaws.services.qldb.model.GetRevisionRequest; import com.amazonaws.services.qldb.model.GetRevisionResult; import java.util.ArrayList; import java.util.List; /** * A Java representation of the {@link Proof} object. * Returned from the {@link com.amazonaws.services.qldb.AmazonQLDB#getRevision(GetRevisionRequest)} api. */ public final class Proof { private static final IonSystem SYSTEM = IonSystemBuilder.standard().build(); private List<byte[]> internalHashes; public Proof(final List<byte[]> internalHashes) { this.internalHashes = internalHashes; } public List<byte[]> getInternalHashes() { return internalHashes; } /** * Decodes a {@link Proof} from an ion text String. This ion text is returned in * a {@link GetRevisionResult#getProof()} * * @param ionText * The ion text representing a {@link Proof} object. * @return {@link JournalBlock} parsed from the ion text. * @throws IllegalStateException if failed to parse the {@link Proof} object from the given ion text. */ public static Proof fromBlob(final String ionText) { try { IonReader reader = SYSTEM.newReader(ionText); List<byte[]> list = new ArrayList<>(); reader.next(); reader.stepIn(); while (reader.next() != null) { list.add(reader.newBytes()); } return new Proof(list); } catch (Exception e) { throw new IllegalStateException("Failed to parse a Proof from byte array"); } } }
    3. QldbIonUtils.java

      /* * 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. */ package software.amazon.qldb.tutorial.qldb; import com.amazon.ion.IonReader; import com.amazon.ion.IonValue; import com.amazon.ionhash.IonHashReader; import com.amazon.ionhash.IonHashReaderBuilder; import com.amazon.ionhash.MessageDigestIonHasherProvider; import software.amazon.qldb.tutorial.Constants; public class QldbIonUtils { private static MessageDigestIonHasherProvider ionHasherProvider = new MessageDigestIonHasherProvider("SHA-256"); private QldbIonUtils() {} /** * Builds a hash value from the given {@link IonValue}. * * @param ionValue * The {@link IonValue} to hash. * @return a byte array representing the hash value. */ public static byte[] hashIonValue(final IonValue ionValue) { IonReader reader = Constants.SYSTEM.newReader(ionValue); IonHashReader hashReader = IonHashReaderBuilder.standard() .withHasherProvider(ionHasherProvider) .withReader(reader) .build(); while (hashReader.next() != null) { } return hashReader.digest(); } }
    4. QldbStringUtils.java

      /* * 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. */ package software.amazon.qldb.tutorial.qldb; import com.amazon.ion.IonWriter; import com.amazon.ion.system.IonReaderBuilder; import com.amazon.ion.system.IonTextWriterBuilder; import com.amazonaws.services.qldb.model.GetBlockResult; import com.amazonaws.services.qldb.model.GetDigestResult; import com.amazonaws.services.qldb.model.ValueHolder; import java.io.IOException; /** * Helper methods to pretty-print certain QLDB response types. */ public class QldbStringUtils { private QldbStringUtils() {} /** * Returns the string representation of a given {@link ValueHolder}. * Adapted from the AWS SDK autogenerated {@code toString()} method, with sensitive values un-redacted. * Additionally, this method pretty-prints any IonText included in the {@link ValueHolder}. * * @param valueHolder the {@link ValueHolder} to convert to a String. * @return the String representation of the supplied {@link ValueHolder}. */ public static String toUnredactedString(ValueHolder valueHolder) { StringBuilder sb = new StringBuilder(); sb.append("{"); if (valueHolder.getIonText() != null) { sb.append("IonText: "); IonWriter prettyWriter = IonTextWriterBuilder.pretty().build(sb); try { prettyWriter.writeValues(IonReaderBuilder.standard().build(valueHolder.getIonText())); } catch (IOException ioe) { sb.append("**Exception while printing this IonText**"); } } sb.append("}"); return sb.toString(); } /** * Returns the string representation of a given {@link GetBlockResult}. * Adapted from the AWS SDK autogenerated {@code toString()} method, with sensitive values un-redacted. * * @param getBlockResult the {@link GetBlockResult} to convert to a String. * @return the String representation of the supplied {@link GetBlockResult}. */ public static String toUnredactedString(GetBlockResult getBlockResult) { StringBuilder sb = new StringBuilder(); sb.append("{"); if (getBlockResult.getBlock() != null) { sb.append("Block: ").append(toUnredactedString(getBlockResult.getBlock())).append(","); } if (getBlockResult.getProof() != null) { sb.append("Proof: ").append(toUnredactedString(getBlockResult.getProof())); } sb.append("}"); return sb.toString(); } /** * Returns the string representation of a given {@link GetDigestResult}. * Adapted from the AWS SDK autogenerated {@code toString()} method, with sensitive values un-redacted. * * @param getDigestResult the {@link GetDigestResult} to convert to a String. * @return the String representation of the supplied {@link GetDigestResult}. */ public static String toUnredactedString(GetDigestResult getDigestResult) { StringBuilder sb = new StringBuilder(); sb.append("{"); if (getDigestResult.getDigest() != null) { sb.append("Digest: ").append(getDigestResult.getDigest()).append(","); } if (getDigestResult.getDigestTipAddress() != null) { sb.append("DigestTipAddress: ").append(toUnredactedString(getDigestResult.getDigestTipAddress())); } sb.append("}"); return sb.toString(); } }
    5. Verifier.java

      2.x
      /* * 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. */ package software.amazon.qldb.tutorial; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.concurrent.ThreadLocalRandom; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.amazonaws.util.Base64; import software.amazon.qldb.tutorial.qldb.Proof; /** * Encapsulates the logic to verify the integrity of revisions or blocks in a QLDB ledger. * * The main entry point is {@link #verify(byte[], byte[], String)}. * * This code expects that you have AWS credentials setup per: * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html */ public final class Verifier { public static final Logger log = LoggerFactory.getLogger(Verifier.class); private static final int HASH_LENGTH = 32; private static final int UPPER_BOUND = 8; /** * Compares two hashes by their <em>signed</em> byte values in little-endian order. */ private static 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 Verifier() { } /** * Verify the integrity of a document with respect to a QLDB ledger digest. * * The verification algorithm includes the following steps: * * 1. {@link #buildCandidateDigest(Proof, byte[])} build the candidate digest from the internal hashes * in the {@link Proof}. * 2. Check that the {@code candidateLedgerDigest} is equal to the {@code ledgerDigest}. * * @param documentHash * The hash of the document to be verified. * @param digest * The QLDB ledger digest. This digest should have been retrieved using * {@link com.amazonaws.services.qldb.AmazonQLDB#getDigest} * @param proofBlob * The ion encoded bytes representing the {@link Proof} associated with the supplied * {@code digestTipAddress} and {@code address} retrieved using * {@link com.amazonaws.services.qldb.AmazonQLDB#getRevision}. * @return {@code true} if the record is verified or {@code false} if it is not verified. */ public static boolean verify( final byte[] documentHash, final byte[] digest, final String proofBlob ) { Proof proof = Proof.fromBlob(proofBlob); byte[] candidateDigest = buildCandidateDigest(proof, documentHash); return Arrays.equals(digest, candidateDigest); } /** * Build the candidate digest representing the entire ledger from the internal hashes of the {@link Proof}. * * @param proof * A Java representation of {@link Proof} * returned from {@link com.amazonaws.services.qldb.AmazonQLDB#getRevision}. * @param leafHash * Leaf hash to build the candidate digest with. * @return a byte array of the candidate digest. */ private static byte[] buildCandidateDigest(final Proof proof, final byte[] leafHash) { return calculateRootHashFromInternalHashes(proof.getInternalHashes(), leafHash); } /** * Get a new instance of {@link MessageDigest} using the SHA-256 algorithm. * * @return an instance of {@link MessageDigest}. * @throws IllegalStateException if the algorithm is not available on the current JVM. */ static MessageDigest newMessageDigest() { try { return MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { log.error("Failed to create SHA-256 MessageDigest", e); throw new IllegalStateException("SHA-256 message digest is unavailable", e); } } /** * Takes two hashes, sorts them, concatenates them, and then returns the * hash of the concatenated array. * * @param h1 * Byte array containing one of the hashes to compare. * @param h2 * Byte array containing one of the hashes to compare. * @return the concatenated array of hashes. */ public static byte[] dot(final byte[] h1, final byte[] h2) { if (h1.length == 0) { return h2; } if (h2.length == 0) { return h1; } byte[] concatenated = new byte[h1.length + h2.length]; if (hashComparator.compare(h1, h2) < 0) { System.arraycopy(h1, 0, concatenated, 0, h1.length); System.arraycopy(h2, 0, concatenated, h1.length, h2.length); } else { System.arraycopy(h2, 0, concatenated, 0, h2.length); System.arraycopy(h1, 0, concatenated, h2.length, h1.length); } MessageDigest messageDigest = newMessageDigest(); messageDigest.update(concatenated); return messageDigest.digest(); } /** * Starting with the provided {@code leafHash} combined with the provided {@code internalHashes} * pairwise until only the root hash remains. * * @param internalHashes * Internal hashes of Merkle tree. * @param leafHash * Leaf hashes of Merkle tree. * @return the root hash. */ private static byte[] calculateRootHashFromInternalHashes(final List<byte[]> internalHashes, final byte[] leafHash) { return internalHashes.stream().reduce(leafHash, Verifier::dot); } /** * Flip a single random bit in the given byte array. This method is used to demonstrate * QLDB's verification features. * * @param original * The original byte array. * @return the altered byte array with a single random bit changed. */ public static byte[] flipRandomBit(final byte[] original) { if (original.length == 0) { throw new IllegalArgumentException("Array cannot be empty!"); } int alteredPosition = ThreadLocalRandom.current().nextInt(original.length); int b = ThreadLocalRandom.current().nextInt(UPPER_BOUND); byte[] altered = new byte[original.length]; System.arraycopy(original, 0, altered, 0, original.length); altered[alteredPosition] = (byte) (altered[alteredPosition] ^ (1 << b)); return altered; } public static String toBase64(byte[] arr) { return new String(Base64.encode(arr), StandardCharsets.UTF_8); } /** * Convert a {@link ByteBuffer} into byte array. * * @param buffer * The {@link ByteBuffer} to convert. * @return the converted byte array. */ public static byte[] convertByteBufferToByteArray(final ByteBuffer buffer) { byte[] arr = new byte[buffer.remaining()]; buffer.get(arr); return arr; } /** * Calculates the root hash from a list of hashes that represent the base of a Merkle tree. * * @param hashes * The list of byte arrays representing hashes making up base of a Merkle tree. * @return a byte array that is the root hash of the given list of hashes. */ public static byte[] calculateMerkleTreeRootHash(List<byte[]> hashes) { if (hashes.isEmpty()) { return new byte[0]; } List<byte[]> remaining = combineLeafHashes(hashes); while (remaining.size() > 1) { remaining = combineLeafHashes(remaining); } return remaining.get(0); } private static List<byte[]> combineLeafHashes(List<byte[]> hashes) { List<byte[]> combinedHashes = new ArrayList<>(); Iterator<byte[]> it = hashes.stream().iterator(); while (it.hasNext()) { byte[] left = it.next(); if (it.hasNext()) { byte[] right = it.next(); byte[] combined = dot(left, right); combinedHashes.add(combined); } else { combinedHashes.add(left); } } return combinedHashes; } }
      1.x
      /* * 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. */ package software.amazon.qldb.tutorial; import com.amazonaws.util.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.qldb.tutorial.qldb.Proof; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; import java.util.concurrent.ThreadLocalRandom; /** * Encapsulates the logic to verify the integrity of revisions or blocks in a QLDB ledger. * * The main entry point is {@link #verify(byte[], byte[], String)}. * * This code expects that you have AWS credentials setup per: * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html */ public final class Verifier { public static final Logger log = LoggerFactory.getLogger(Verifier.class); private static final int HASH_LENGTH = 32; private static final int UPPER_BOUND = 8; /** * Compares two hashes by their <em>signed</em> byte values in little-endian order. */ private static 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 Verifier() { } /** * Verify the integrity of a document with respect to a QLDB ledger digest. * * The verification algorithm includes the following steps: * * 1. {@link #buildCandidateDigest(Proof, byte[])} build the candidate digest from the internal hashes * in the {@link Proof}. * 2. Check that the {@code candidateLedgerDigest} is equal to the {@code ledgerDigest}. * * @param documentHash * The hash of the document to be verified. * @param digest * The QLDB ledger digest. This digest should have been retrieved using * {@link com.amazonaws.services.qldb.AmazonQLDB#getDigest} * @param proofBlob * The ion encoded bytes representing the {@link Proof} associated with the supplied * {@code digestTipAddress} and {@code address} retrieved using * {@link com.amazonaws.services.qldb.AmazonQLDB#getRevision}. * @return {@code true} if the record is verified or {@code false} if it is not verified. */ public static boolean verify( final byte[] documentHash, final byte[] digest, final String proofBlob ) { Proof proof = Proof.fromBlob(proofBlob); byte[] candidateDigest = buildCandidateDigest(proof, documentHash); return Arrays.equals(digest, candidateDigest); } /** * Build the candidate digest representing the entire ledger from the internal hashes of the {@link Proof}. * * @param proof * A Java representation of {@link Proof} * returned from {@link com.amazonaws.services.qldb.AmazonQLDB#getRevision}. * @param leafHash * Leaf hash to build the candidate digest with. * @return a byte array of the candidate digest. */ private static byte[] buildCandidateDigest(final Proof proof, final byte[] leafHash) { return calculateRootHashFromInternalHashes(proof.getInternalHashes(), leafHash); } /** * Get a new instance of {@link MessageDigest} using the SHA-256 algorithm. * * @return an instance of {@link MessageDigest}. * @throws IllegalStateException if the algorithm is not available on the current JVM. */ static MessageDigest newMessageDigest() { try { return MessageDigest.getInstance("SHA-256"); } catch (NoSuchAlgorithmException e) { log.error("Failed to create SHA-256 MessageDigest", e); throw new IllegalStateException("SHA-256 message digest is unavailable", e); } } /** * Takes two hashes, sorts them, concatenates them, and then returns the * hash of the concatenated array. * * @param h1 * Byte array containing one of the hashes to compare. * @param h2 * Byte array containing one of the hashes to compare. * @return the concatenated array of hashes. */ public static byte[] dot(final byte[] h1, final byte[] h2) { if (h1.length == 0) { return h2; } if (h2.length == 0) { return h1; } byte[] concatenated = new byte[h1.length + h2.length]; if (hashComparator.compare(h1, h2) < 0) { System.arraycopy(h1, 0, concatenated, 0, h1.length); System.arraycopy(h2, 0, concatenated, h1.length, h2.length); } else { System.arraycopy(h2, 0, concatenated, 0, h2.length); System.arraycopy(h1, 0, concatenated, h2.length, h1.length); } MessageDigest messageDigest = newMessageDigest(); messageDigest.update(concatenated); return messageDigest.digest(); } /** * Starting with the provided {@code leafHash} combined with the provided {@code internalHashes} * pairwise until only the root hash remains. * * @param internalHashes * Internal hashes of Merkle tree. * @param leafHash * Leaf hashes of Merkle tree. * @return the root hash. */ private static byte[] calculateRootHashFromInternalHashes(final List<byte[]> internalHashes, final byte[] leafHash) { return internalHashes.stream().reduce(leafHash, Verifier::dot); } /** * Flip a single random bit in the given byte array. This method is used to demonstrate * QLDB's verification features. * * @param original * The original byte array. * @return the altered byte array with a single random bit changed. */ public static byte[] flipRandomBit(final byte[] original) { if (original.length == 0) { throw new IllegalArgumentException("Array cannot be empty!"); } int alteredPosition = ThreadLocalRandom.current().nextInt(original.length); int b = ThreadLocalRandom.current().nextInt(UPPER_BOUND); byte[] altered = new byte[original.length]; System.arraycopy(original, 0, altered, 0, original.length); altered[alteredPosition] = (byte) (altered[alteredPosition] ^ (1 << b)); return altered; } public static String toBase64(byte[] arr) { return new String(Base64.encode(arr), StandardCharsets.UTF_8); } /** * Convert a {@link ByteBuffer} into byte array. * * @param buffer * The {@link ByteBuffer} to convert. * @return the converted byte array. */ public static byte[] convertByteBufferToByteArray(final ByteBuffer buffer) { byte[] arr = new byte[buffer.remaining()]; buffer.get(arr); return arr; } /** * Calculates the root hash from a list of hashes that represent the base of a Merkle tree. * * @param hashes * The list of byte arrays representing hashes making up base of a Merkle tree. * @return a byte array that is the root hash of the given list of hashes. */ public static byte[] calculateMerkleTreeRootHash(List<byte[]> hashes) { if (hashes.isEmpty()) { return new byte[0]; } List<byte[]> remaining = combineLeafHashes(hashes); while (remaining.size() > 1) { remaining = combineLeafHashes(remaining); } return remaining.get(0); } private static List<byte[]> combineLeafHashes(List<byte[]> hashes) { List<byte[]> combinedHashes = new ArrayList<>(); Iterator<byte[]> it = hashes.stream().iterator(); while (it.hasNext()) { byte[] left = it.next(); if (it.hasNext()) { byte[] right = it.next(); byte[] combined = dot(left, right); combinedHashes.add(combined); } else { combinedHashes.add(left); } } return combinedHashes; } }
  2. 2 つの .java ファイル (GetDigest.java および GetRevision.java) を使用して、次の手順を実行します。

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

    • VehicleRegistration テーブルにドキュメントの各リビジョンの証明をリクエストします。

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

    GetDigest.java プログラムには、次のコードが含まれています。

    /* * 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. */ package software.amazon.qldb.tutorial; import com.amazonaws.services.qldb.AmazonQLDB; import com.amazonaws.services.qldb.model.GetDigestRequest; import com.amazonaws.services.qldb.model.GetDigestResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.qldb.tutorial.qldb.QldbStringUtils; /** * This is an example for retrieving the digest of a particular ledger. * * This code expects that you have AWS credentials setup per: * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html */ public final class GetDigest { public static final Logger log = LoggerFactory.getLogger(GetDigest.class); public static AmazonQLDB client = CreateLedger.getClient(); private GetDigest() { } /** * Calls {@link #getDigest(String)} for a ledger. * * @param args * Arbitrary command-line arguments. * @throws Exception if failed to get a ledger digest. */ public static void main(final String... args) throws Exception { try { getDigest(Constants.LEDGER_NAME); } catch (Exception e) { log.error("Unable to get a ledger digest!", e); throw e; } } /** * Get the digest for the specified ledger. * * @param ledgerName * The ledger to get digest from. * @return {@link GetDigestResult}. */ public static GetDigestResult getDigest(final String ledgerName) { log.info("Let's get the current digest of the ledger named {}.", ledgerName); GetDigestRequest request = new GetDigestRequest() .withName(ledgerName); GetDigestResult result = client.getDigest(request); log.info("Success. LedgerDigest: {}.", QldbStringUtils.toUnredactedString(result)); return result; } }
    注記

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

    GetRevision.java プログラムには、次のコードが含まれています。

    2.x
    /* * 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. */ package software.amazon.qldb.tutorial; import com.amazon.ion.IonReader; import com.amazon.ion.IonStruct; import com.amazon.ion.IonSystem; import com.amazon.ion.IonValue; import com.amazon.ion.IonWriter; import com.amazon.ion.system.IonReaderBuilder; import com.amazon.ion.system.IonSystemBuilder; import com.amazonaws.services.qldb.AmazonQLDB; import com.amazonaws.services.qldb.model.GetDigestResult; import com.amazonaws.services.qldb.model.GetRevisionRequest; import com.amazonaws.services.qldb.model.GetRevisionResult; import com.amazonaws.services.qldb.model.ValueHolder; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.qldb.QldbDriver; import software.amazon.qldb.Result; import software.amazon.qldb.TransactionExecutor; import software.amazon.qldb.tutorial.model.SampleData; import software.amazon.qldb.tutorial.qldb.BlockAddress; import software.amazon.qldb.tutorial.qldb.QldbRevision; import software.amazon.qldb.tutorial.qldb.QldbStringUtils; /** * Verify the integrity of a document revision in a QLDB ledger. * * This code expects that you have AWS credentials setup per: * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html */ public final class GetRevision { public static final Logger log = LoggerFactory.getLogger(GetRevision.class); public static AmazonQLDB client = CreateLedger.getClient(); private static final IonSystem SYSTEM = IonSystemBuilder.standard().build(); private GetRevision() { } public static void main(String... args) throws Exception { final String vin = SampleData.REGISTRATIONS.get(0).getVin(); verifyRegistration(ConnectToLedger.getDriver(), Constants.LEDGER_NAME, vin); } /** * Verify each version of the registration for the given VIN. * * @param driver * A QLDB driver. * @param ledgerName * The ledger to get digest from. * @param vin * VIN to query the revision history of a specific registration with. * @throws Exception if failed to verify digests. * @throws AssertionError if document revision verification failed. */ public static void verifyRegistration(final QldbDriver driver, final String ledgerName, final String vin) throws Exception { log.info(String.format("Let's verify the registration with VIN=%s, in ledger=%s.", vin, ledgerName)); try { log.info("First, let's get a digest."); GetDigestResult digestResult = GetDigest.getDigest(ledgerName); ValueHolder digestTipAddress = digestResult.getDigestTipAddress(); byte[] digestBytes = Verifier.convertByteBufferToByteArray(digestResult.getDigest()); log.info("Got a ledger digest. Digest end address={}, digest={}.", QldbStringUtils.toUnredactedString(digestTipAddress), Verifier.toBase64(digestBytes)); log.info(String.format("Next, let's query the registration with VIN=%s. " + "Then we can verify each version of the registration.", vin)); List<IonStruct> documentsWithMetadataList = new ArrayList<>(); driver.execute(txn -> { documentsWithMetadataList.addAll(queryRegistrationsByVin(txn, vin)); }); log.info("Registrations queried successfully!"); log.info(String.format("Found %s revisions of the registration with VIN=%s.", documentsWithMetadataList.size(), vin)); for (IonStruct ionStruct : documentsWithMetadataList) { QldbRevision document = QldbRevision.fromIon(ionStruct); log.info(String.format("Let's verify the document: %s", document)); log.info("Let's get a proof for the document."); GetRevisionResult proofResult = getRevision( ledgerName, document.getMetadata().getId(), digestTipAddress, document.getBlockAddress() ); final IonValue proof = Constants.MAPPER.writeValueAsIonValue(proofResult.getProof()); final IonReader reader = IonReaderBuilder.standard().build(proof); reader.next(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); IonWriter writer = SYSTEM.newBinaryWriter(baos); writer.writeValue(reader); writer.close(); baos.flush(); baos.close(); byte[] byteProof = baos.toByteArray(); log.info(String.format("Got back a proof: %s", Verifier.toBase64(byteProof))); boolean verified = Verifier.verify( document.getHash(), digestBytes, proofResult.getProof().getIonText() ); if (!verified) { throw new AssertionError("Document revision is not verified!"); } else { log.info("Success! The document is verified"); } byte[] alteredDigest = Verifier.flipRandomBit(digestBytes); log.info(String.format("Flipping one bit in the digest and assert that the document is NOT verified. " + "The altered digest is: %s", Verifier.toBase64(alteredDigest))); verified = Verifier.verify( document.getHash(), alteredDigest, proofResult.getProof().getIonText() ); if (verified) { throw new AssertionError("Expected document to not be verified against altered digest."); } else { log.info("Success! As expected flipping a bit in the digest causes verification to fail."); } byte[] alteredDocumentHash = Verifier.flipRandomBit(document.getHash()); log.info(String.format("Flipping one bit in the document's hash and assert that it is NOT verified. " + "The altered document hash is: %s.", Verifier.toBase64(alteredDocumentHash))); verified = Verifier.verify( alteredDocumentHash, digestBytes, proofResult.getProof().getIonText() ); if (verified) { throw new AssertionError("Expected altered document hash to not be verified against digest."); } else { log.info("Success! As expected flipping a bit in the document hash causes verification to fail."); } } } catch (Exception e) { log.error("Failed to verify digests.", e); throw e; } log.info(String.format("Finished verifying the registration with VIN=%s in ledger=%s.", vin, ledgerName)); } /** * Get the revision of a particular document specified by the given document ID and block address. * * @param ledgerName * Name of the ledger containing the document. * @param documentId * Unique ID for the document to be verified, contained in the committed view of the document. * @param digestTipAddress * The latest block location covered by the digest. * @param blockAddress * The location of the block to request. * @return the requested revision. */ public static GetRevisionResult getRevision(final String ledgerName, final String documentId, final ValueHolder digestTipAddress, final BlockAddress blockAddress) { try { GetRevisionRequest request = new GetRevisionRequest() .withName(ledgerName) .withDigestTipAddress(digestTipAddress) .withBlockAddress(new ValueHolder().withIonText(Constants.MAPPER.writeValueAsIonValue(blockAddress) .toString())) .withDocumentId(documentId); return client.getRevision(request); } catch (IOException ioe) { throw new IllegalStateException(ioe); } } /** * Query the registration history for the given VIN. * * @param txn * The {@link TransactionExecutor} for lambda execute. * @param vin * The unique VIN to query. * @return a list of {@link IonStruct} representing the registration history. * @throws IllegalStateException if failed to convert parameters into {@link IonValue} */ public static List<IonStruct> queryRegistrationsByVin(final TransactionExecutor txn, final String vin) { log.info(String.format("Let's query the 'VehicleRegistration' table for VIN: %s...", vin)); log.info("Let's query the 'VehicleRegistration' table for VIN: {}...", vin); final String query = String.format("SELECT * FROM _ql_committed_%s WHERE data.VIN = ?", Constants.VEHICLE_REGISTRATION_TABLE_NAME); try { final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(vin)); final Result result = txn.execute(query, parameters); List<IonStruct> list = ScanTable.toIonStructs(result); log.info(String.format("Found %d document(s)!", list.size())); return list; } catch (IOException ioe) { throw new IllegalStateException(ioe); } } }
    1.x
    /* * 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. */ package software.amazon.qldb.tutorial; import com.amazon.ion.IonReader; import com.amazon.ion.IonStruct; import com.amazon.ion.IonValue; import com.amazon.ion.IonWriter; import com.amazon.ion.system.IonReaderBuilder; import com.amazonaws.services.qldb.AmazonQLDB; import com.amazonaws.services.qldb.model.GetDigestResult; import com.amazonaws.services.qldb.model.GetRevisionRequest; import com.amazonaws.services.qldb.model.GetRevisionResult; import com.amazonaws.services.qldb.model.ValueHolder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.qldb.QldbSession; import software.amazon.qldb.Result; import software.amazon.qldb.TransactionExecutor; import software.amazon.qldb.tutorial.model.SampleData; import software.amazon.qldb.tutorial.qldb.BlockAddress; import software.amazon.qldb.tutorial.qldb.QldbRevision; import software.amazon.qldb.tutorial.qldb.QldbStringUtils; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Verify the integrity of a document revision in a QLDB ledger. * * This code expects that you have AWS credentials setup per: * http://docs.aws.amazon.com/java-sdk/latest/developer-guide/setup-credentials.html */ public final class GetRevision { public static final Logger log = LoggerFactory.getLogger(GetRevision.class); public static AmazonQLDB client = CreateLedger.getClient(); private GetRevision() { } public static void main(String... args) throws Exception { final String vin = SampleData.REGISTRATIONS.get(0).getVin(); try (QldbSession qldbSession = ConnectToLedger.createQldbSession()) { verifyRegistration(qldbSession, Constants.LEDGER_NAME, vin); } } /** * Verify each version of the registration for the given VIN. * * @param qldbSession * A QLDB session. * @param ledgerName * The ledger to get digest from. * @param vin * VIN to query the revision history of a specific registration with. * @throws Exception if failed to verify digests. * @throws AssertionError if document revision verification failed. */ public static void verifyRegistration(final QldbSession qldbSession, final String ledgerName, final String vin) throws Exception { log.info(String.format("Let's verify the registration with VIN=%s, in ledger=%s.", vin, ledgerName)); try { log.info("First, let's get a digest."); GetDigestResult digestResult = GetDigest.getDigest(ledgerName); ValueHolder digestTipAddress = digestResult.getDigestTipAddress(); byte[] digestBytes = Verifier.convertByteBufferToByteArray(digestResult.getDigest()); log.info("Got a ledger digest. Digest end address={}, digest={}.", QldbStringUtils.toUnredactedString(digestTipAddress), Verifier.toBase64(digestBytes)); log.info(String.format("Next, let's query the registration with VIN=%s. " + "Then we can verify each version of the registration.", vin)); List<IonStruct> documentsWithMetadataList = new ArrayList<>(); qldbSession.execute(txn -> { documentsWithMetadataList.addAll(queryRegistrationsByVin(txn, vin)); }, (retryAttempt) -> log.info("Retrying due to OCC conflict...")); log.info("Registrations queried successfully!"); log.info(String.format("Found %s revisions of the registration with VIN=%s.", documentsWithMetadataList.size(), vin)); for (IonStruct ionStruct : documentsWithMetadataList) { QldbRevision document = QldbRevision.fromIon(ionStruct); log.info(String.format("Let's verify the document: %s", document)); log.info("Let's get a proof for the document."); GetRevisionResult proofResult = getRevision( ledgerName, document.getMetadata().getId(), digestTipAddress, document.getBlockAddress() ); final IonValue proof = Constants.MAPPER.writeValueAsIonValue(proofResult.getProof()); final IonReader reader = IonReaderBuilder.standard().build(proof); reader.next(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); IonWriter writer = Constants.SYSTEM.newBinaryWriter(baos); writer.writeValue(reader); writer.close(); baos.flush(); baos.close(); byte[] byteProof = baos.toByteArray(); log.info(String.format("Got back a proof: %s", Verifier.toBase64(byteProof))); boolean verified = Verifier.verify( document.getHash(), digestBytes, proofResult.getProof().getIonText() ); if (!verified) { throw new AssertionError("Document revision is not verified!"); } else { log.info("Success! The document is verified"); } byte[] alteredDigest = Verifier.flipRandomBit(digestBytes); log.info(String.format("Flipping one bit in the digest and assert that the document is NOT verified. " + "The altered digest is: %s", Verifier.toBase64(alteredDigest))); verified = Verifier.verify( document.getHash(), alteredDigest, proofResult.getProof().getIonText() ); if (verified) { throw new AssertionError("Expected document to not be verified against altered digest."); } else { log.info("Success! As expected flipping a bit in the digest causes verification to fail."); } byte[] alteredDocumentHash = Verifier.flipRandomBit(document.getHash()); log.info(String.format("Flipping one bit in the document's hash and assert that it is NOT verified. " + "The altered document hash is: %s.", Verifier.toBase64(alteredDocumentHash))); verified = Verifier.verify( alteredDocumentHash, digestBytes, proofResult.getProof().getIonText() ); if (verified) { throw new AssertionError("Expected altered document hash to not be verified against digest."); } else { log.info("Success! As expected flipping a bit in the document hash causes verification to fail."); } } } catch (Exception e) { log.error("Failed to verify digests.", e); throw e; } log.info(String.format("Finished verifying the registration with VIN=%s in ledger=%s.", vin, ledgerName)); } /** * Get the revision of a particular document specified by the given document ID and block address. * * @param ledgerName * Name of the ledger containing the document. * @param documentId * Unique ID for the document to be verified, contained in the committed view of the document. * @param digestTipAddress * The latest block location covered by the digest. * @param blockAddress * The location of the block to request. * @return the requested revision. */ public static GetRevisionResult getRevision(final String ledgerName, final String documentId, final ValueHolder digestTipAddress, final BlockAddress blockAddress) { try { GetRevisionRequest request = new GetRevisionRequest() .withName(ledgerName) .withDigestTipAddress(digestTipAddress) .withBlockAddress(new ValueHolder().withIonText(Constants.MAPPER.writeValueAsIonValue(blockAddress) .toString())) .withDocumentId(documentId); return client.getRevision(request); } catch (IOException ioe) { throw new IllegalStateException(ioe); } } /** * Query the registration history for the given VIN. * * @param txn * The {@link TransactionExecutor} for lambda execute. * @param vin * The unique VIN to query. * @return a list of {@link IonStruct} representing the registration history. * @throws IllegalStateException if failed to convert parameters into {@link IonValue} */ public static List<IonStruct> queryRegistrationsByVin(final TransactionExecutor txn, final String vin) { log.info(String.format("Let's query the 'VehicleRegistration' table for VIN: %s...", vin)); log.info("Let's query the 'VehicleRegistration' table for VIN: {}...", vin); final String query = String.format("SELECT * FROM _ql_committed_%s WHERE data.VIN = ?", Constants.VEHICLE_REGISTRATION_TABLE_NAME); try { final List<IonValue> parameters = Collections.singletonList(Constants.MAPPER.writeValueAsIonValue(vin)); final Result result = txn.execute(query, parameters); List<IonStruct> list = ScanTable.toIonStructs(result); log.info(String.format("Found %d document(s)!", list.size())); return list; } catch (IOException ioe) { throw new IllegalStateException(ioe); } } }
    注記

    getRevision メソッドが指定されたドキュメントリビジョンの証明を返した後、このプログラムはクライアント側APIを使用してそのリビジョンを検証します。この で使用されるアルゴリズムの概要については、API「」を参照してください証明を使用したダイジェストの再計算

  3. プログラムをコンパイルして実行GetRevision.javaし、 VIN でVehicleRegistrationドキュメントを暗号的に検証します1N4AL11D75C109151

vehicle-registration 台帳でジャーナルデータをエクスポートして検証するには、「ステップ 8: 元帳のジャーナルデータをエクスポートして検証する」に進みます。