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 vehicle-registration ledger. 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. Examine os arquivos .java a seguir, que representam objetos QLDB necessários para verificação e classes utilitárias com métodos auxiliares para valores de Ion e string.

    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. Use dois .java arquivos (GetDigest.java e GetRevision.java) para realizar as seguintes etapas:

    • Solicite um novo resumo do ledger vehicle-registration.

    • Solicite uma prova para cada revisão de um documento da tabela VehicleRegistration.

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

    O programa GetDigest.java 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. */ 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; } }
    nota

    Use o método 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.java contém o código a seguir.

    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); } } }
    nota

    Depois que o método getRevision retorna uma prova da revisão do documento especificada, esse programa usa uma API do lado do cliente para verificar essa revisão. Para obter uma visão geral do algoritmo usado por essa API, consulte Usando uma prova para recalcular seu resumo.

  3. Compile e execute o programa GetRevision.java para verificar criptograficamente o documento VehicleRegistration com o VIN. 1N4AL11D75C109151

Para exportar e validar os dados do diário no vehicle-registration ledger, vá para. Etapa 8: exportar e validar dados do diário em um ledger