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

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

Paso 7: Verificar un documento en un libro mayor

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

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

Para comprobar la revisión de un documento

  1. Revise los siguientes.java archivos, que representan los objetos de QLDB que se requieren para la verificación y una clase de utilidad con métodos de ayuda para imprimir de forma bonita algunos tipos de respuestas de QLDB.

    2.x
    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. QldbRevision.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.IonBlob; import com.amazon.ion.IonStruct; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.qldb.tutorial.Constants; import software.amazon.qldb.tutorial.Verifier; import java.io.IOException; import java.util.Arrays; import java.util.Objects; /** * Represents a QldbRevision including both user data and metadata. */ public final class QldbRevision { private static final Logger log = LoggerFactory.getLogger(QldbRevision.class); private final BlockAddress blockAddress; private final RevisionMetadata metadata; private final byte[] hash; private final IonStruct data; @JsonCreator public QldbRevision(@JsonProperty("blockAddress") final BlockAddress blockAddress, @JsonProperty("metadata") final RevisionMetadata metadata, @JsonProperty("hash") final byte[] hash, @JsonProperty("data") final IonStruct data) { this.blockAddress = blockAddress; this.metadata = metadata; this.hash = hash; this.data = data; } /** * Gets the unique ID of a QLDB document. * * @return the {@link BlockAddress} object. */ public BlockAddress getBlockAddress() { return blockAddress; } /** * Gets the metadata of the revision. * * @return the {@link RevisionMetadata} object. */ public RevisionMetadata getMetadata() { return metadata; } /** * Gets the SHA-256 hash value of the data. * * @return the byte array representing the hash. */ public byte[] getHash() { return hash; } /** * Gets the revision data. * * @return the revision data. */ public IonStruct getData() { return data; } /** * Constructs a new {@link QldbRevision} from an {@link IonStruct}. * * The specified {@link IonStruct} must include the following fields * * - blockAddress -- a {@link BlockAddress}, * - metadata -- a {@link RevisionMetadata}, * - hash -- the document's hash calculated by QLDB, * - data -- an {@link IonStruct} containing user data in the document. * * If any of these fields are missing or are malformed, then throws {@link IllegalArgumentException}. * * If the document hash calculated from the members of the specified {@link IonStruct} does not match * the hash member of the {@link IonStruct} then throws {@link IllegalArgumentException}. * * @param ionStruct * The {@link IonStruct} that contains a {@link QldbRevision} object. * @return the converted {@link QldbRevision} object. * @throws IOException if failed to parse parameter {@link IonStruct}. */ public static QldbRevision fromIon(final IonStruct ionStruct) throws IOException { try { BlockAddress blockAddress = Constants.MAPPER.readValue(ionStruct.get("blockAddress"), BlockAddress.class); IonBlob hash = (IonBlob) ionStruct.get("hash"); IonStruct metadataStruct = (IonStruct) ionStruct.get("metadata"); IonStruct data = (IonStruct) ionStruct.get("data"); if (hash == null || data == null) { throw new IllegalArgumentException("Document is missing required fields"); } verifyRevisionHash(metadataStruct, data, hash.getBytes()); RevisionMetadata metadata = RevisionMetadata.fromIon(metadataStruct); return new QldbRevision(blockAddress, metadata, hash.getBytes(), data); } catch (ClassCastException e) { log.error("Failed to parse ion document"); throw new IllegalArgumentException("Document members are not of the correct type", e); } } /** * Converts a {@link QldbRevision} object to string. * * @return the string representation of the {@link QldbRevision} object. */ @Override public String toString() { return "QldbRevision{" + "blockAddress=" + blockAddress + ", metadata=" + metadata + ", hash=" + Arrays.toString(hash) + ", data=" + data + '}'; } /** * Check whether two {@link QldbRevision} objects are equivalent. * * @return {@code true} if the two objects are equal, {@code false} otherwise. */ @Override public boolean equals(final Object o) { if (this == o) { return true; } if (!(o instanceof QldbRevision)) { return false; } final QldbRevision that = (QldbRevision) o; return Objects.equals(getBlockAddress(), that.getBlockAddress()) && Objects.equals(getMetadata(), that.getMetadata()) && Arrays.equals(getHash(), that.getHash()) && Objects.equals(getData(), that.getData()); } /** * Create a hash code for the {@link QldbRevision} object. * * @return the hash code. */ @Override public int hashCode() { // CHECKSTYLE:OFF - Disabling as we are generating a hashCode of multiple properties. int result = Objects.hash(blockAddress, metadata, data); // CHECKSTYLE:ON result = 31 * result + Arrays.hashCode(hash); return result; } /** * Throws an IllegalArgumentException if the hash of the revision data and metadata * does not match the hash provided by QLDB with the revision. */ public void verifyRevisionHash() { // Certain internal-only system revisions only contain a hash which cannot be // further computed. However, these system hashes still participate to validate // the journal block. if (blockAddress == null || metadata == null || data == null) { return; } try { IonStruct metadataIon = (IonStruct) Constants.MAPPER.writeValueAsIonValue(metadata); verifyRevisionHash(metadataIon, data, hash); } catch (IOException e) { throw new IllegalArgumentException("Could not encode revision metadata to ion.", e); } } private static void verifyRevisionHash(IonStruct metadata, IonStruct revisionData, byte[] expectedHash) { byte[] metadataHash = QldbIonUtils.hashIonValue(metadata); byte[] dataHash = QldbIonUtils.hashIonValue(revisionData); byte[] candidateHash = Verifier.dot(metadataHash, dataHash); if (!Arrays.equals(candidateHash, expectedHash)) { throw new IllegalArgumentException("Hash entry of QLDB revision and computed hash " + "of QLDB revision do not match"); } } }
    4. RevisionMetadata.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.IonInt; import com.amazon.ion.IonString; import com.amazon.ion.IonStruct; import com.amazon.ion.IonTimestamp; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.dataformat.ion.IonTimestampSerializers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Date; import java.util.Objects; /** * Represents the metadata field of a QLDB Document */ public class RevisionMetadata { private static final Logger log = LoggerFactory.getLogger(RevisionMetadata.class); private final String id; private final long version; @JsonSerialize(using = IonTimestampSerializers.IonTimestampJavaDateSerializer.class) private final Date txTime; private final String txId; @JsonCreator public RevisionMetadata(@JsonProperty("id") final String id, @JsonProperty("version") final long version, @JsonProperty("txTime") final Date txTime, @JsonProperty("txId") final String txId) { this.id = id; this.version = version; this.txTime = txTime; this.txId = txId; } /** * Gets the unique ID of a QLDB document. * * @return the document ID. */ public String getId() { return id; } /** * Gets the version number of the document in the document's modification history. * @return the version number. */ public long getVersion() { return version; } /** * Gets the time during which the document was modified. * * @return the transaction time. */ public Date getTxTime() { return txTime; } /** * Gets the transaction ID associated with this document. * * @return the transaction ID. */ public String getTxId() { return txId; } public static RevisionMetadata fromIon(final IonStruct ionStruct) { if (ionStruct == null) { throw new IllegalArgumentException("Metadata cannot be null"); } try { IonString id = (IonString) ionStruct.get("id"); IonInt version = (IonInt) ionStruct.get("version"); IonTimestamp txTime = (IonTimestamp) ionStruct.get("txTime"); IonString txId = (IonString) ionStruct.get("txId"); if (id == null || version == null || txTime == null || txId == null) { throw new IllegalArgumentException("Document is missing required fields"); } return new RevisionMetadata(id.stringValue(), version.longValue(), new Date(txTime.getMillis()), txId.stringValue()); } catch (ClassCastException e) { log.error("Failed to parse ion document"); throw new IllegalArgumentException("Document members are not of the correct type", e); } } /** * Converts a {@link RevisionMetadata} object to a string. * * @return the string representation of the {@link QldbRevision} object. */ @Override public String toString() { return "Metadata{" + "id='" + id + '\'' + ", version=" + version + ", txTime=" + txTime + ", txId='" + txId + '\'' + '}'; } /** * Check whether two {@link RevisionMetadata} objects are equivalent. * * @return {@code true} if the two objects are equal, {@code false} otherwise. */ @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } RevisionMetadata metadata = (RevisionMetadata) o; return version == metadata.version && id.equals(metadata.id) && txTime.equals(metadata.txTime) && txId.equals(metadata.txId); } /** * Generate a hash code for the {@link RevisionMetadata} object. * * @return the hash code. */ @Override public int hashCode() { // CHECKSTYLE:OFF - Disabling as we are generating a hashCode of multiple properties. return Objects.hash(id, version, txTime, txId); // CHECKSTYLE:ON } }
    5. Verifier.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 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; } }
    6. 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(); } }
    7. 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(); } }
    1.x
    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. QldbRevision.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.IonBlob; import com.amazon.ion.IonStruct; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.qldb.tutorial.Constants; import software.amazon.qldb.tutorial.Verifier; import java.io.IOException; import java.util.Arrays; import java.util.Objects; /** * Represents a QldbRevision including both user data and metadata. */ public final class QldbRevision { private static final Logger log = LoggerFactory.getLogger(QldbRevision.class); private final BlockAddress blockAddress; private final RevisionMetadata metadata; private final byte[] hash; private final IonStruct data; @JsonCreator public QldbRevision(@JsonProperty("blockAddress") final BlockAddress blockAddress, @JsonProperty("metadata") final RevisionMetadata metadata, @JsonProperty("hash") final byte[] hash, @JsonProperty("data") final IonStruct data) { this.blockAddress = blockAddress; this.metadata = metadata; this.hash = hash; this.data = data; } /** * Gets the unique ID of a QLDB document. * * @return the {@link BlockAddress} object. */ public BlockAddress getBlockAddress() { return blockAddress; } /** * Gets the metadata of the revision. * * @return the {@link RevisionMetadata} object. */ public RevisionMetadata getMetadata() { return metadata; } /** * Gets the SHA-256 hash value of the data. * * @return the byte array representing the hash. */ public byte[] getHash() { return hash; } /** * Gets the revision data. * * @return the revision data. */ public IonStruct getData() { return data; } /** * Constructs a new {@link QldbRevision} from an {@link IonStruct}. * * The specified {@link IonStruct} must include the following fields * * - blockAddress -- a {@link BlockAddress}, * - metadata -- a {@link RevisionMetadata}, * - hash -- the document's hash calculated by QLDB, * - data -- an {@link IonStruct} containing user data in the document. * * If any of these fields are missing or are malformed, then throws {@link IllegalArgumentException}. * * If the document hash calculated from the members of the specified {@link IonStruct} does not match * the hash member of the {@link IonStruct} then throws {@link IllegalArgumentException}. * * @param ionStruct * The {@link IonStruct} that contains a {@link QldbRevision} object. * @return the converted {@link QldbRevision} object. * @throws IOException if failed to parse parameter {@link IonStruct}. */ public static QldbRevision fromIon(final IonStruct ionStruct) throws IOException { try { BlockAddress blockAddress = Constants.MAPPER.readValue(ionStruct.get("blockAddress"), BlockAddress.class); IonBlob hash = (IonBlob) ionStruct.get("hash"); IonStruct metadataStruct = (IonStruct) ionStruct.get("metadata"); IonStruct data = (IonStruct) ionStruct.get("data"); if (hash == null || data == null) { throw new IllegalArgumentException("Document is missing required fields"); } verifyRevisionHash(metadataStruct, data, hash.getBytes()); RevisionMetadata metadata = RevisionMetadata.fromIon(metadataStruct); return new QldbRevision(blockAddress, metadata, hash.getBytes(), data); } catch (ClassCastException e) { log.error("Failed to parse ion document"); throw new IllegalArgumentException("Document members are not of the correct type", e); } } /** * Converts a {@link QldbRevision} object to string. * * @return the string representation of the {@link QldbRevision} object. */ @Override public String toString() { return "QldbRevision{" + "blockAddress=" + blockAddress + ", metadata=" + metadata + ", hash=" + Arrays.toString(hash) + ", data=" + data + '}'; } /** * Check whether two {@link QldbRevision} objects are equivalent. * * @return {@code true} if the two objects are equal, {@code false} otherwise. */ @Override public boolean equals(final Object o) { if (this == o) { return true; } if (!(o instanceof QldbRevision)) { return false; } final QldbRevision that = (QldbRevision) o; return Objects.equals(getBlockAddress(), that.getBlockAddress()) && Objects.equals(getMetadata(), that.getMetadata()) && Arrays.equals(getHash(), that.getHash()) && Objects.equals(getData(), that.getData()); } /** * Create a hash code for the {@link QldbRevision} object. * * @return the hash code. */ @Override public int hashCode() { // CHECKSTYLE:OFF - Disabling as we are generating a hashCode of multiple properties. int result = Objects.hash(blockAddress, metadata, data); // CHECKSTYLE:ON result = 31 * result + Arrays.hashCode(hash); return result; } /** * Throws an IllegalArgumentException if the hash of the revision data and metadata * does not match the hash provided by QLDB with the revision. */ public void verifyRevisionHash() { // Certain internal-only system revisions only contain a hash which cannot be // further computed. However, these system hashes still participate to validate // the journal block. User revisions will always contain values for all fields // and can therefore have their hash computed. if (blockAddress == null && metadata == null && data == null) { return; } try { IonStruct metadataIon = (IonStruct) Constants.MAPPER.writeValueAsIonValue(metadata); verifyRevisionHash(metadataIon, data, hash); } catch (IOException e) { throw new IllegalArgumentException("Could not encode revision metadata to ion.", e); } } private static void verifyRevisionHash(IonStruct metadata, IonStruct revisionData, byte[] expectedHash) { byte[] metadataHash = QldbIonUtils.hashIonValue(metadata); byte[] dataHash = QldbIonUtils.hashIonValue(revisionData); byte[] candidateHash = Verifier.dot(metadataHash, dataHash); if (!Arrays.equals(candidateHash, expectedHash)) { throw new IllegalArgumentException("Hash entry of QLDB revision and computed hash " + "of QLDB revision do not match"); } } }
    4. RevisionMetadata.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.IonInt; import com.amazon.ion.IonString; import com.amazon.ion.IonStruct; import com.amazon.ion.IonTimestamp; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.dataformat.ion.IonTimestampSerializers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Date; import java.util.Objects; /** * Represents the metadata field of a QLDB Document */ public class RevisionMetadata { private static final Logger log = LoggerFactory.getLogger(RevisionMetadata.class); private final String id; private final long version; @JsonSerialize(using = IonTimestampSerializers.IonTimestampJavaDateSerializer.class) private final Date txTime; private final String txId; @JsonCreator public RevisionMetadata(@JsonProperty("id") final String id, @JsonProperty("version") final long version, @JsonProperty("txTime") final Date txTime, @JsonProperty("txId") final String txId) { this.id = id; this.version = version; this.txTime = txTime; this.txId = txId; } /** * Gets the unique ID of a QLDB document. * * @return the document ID. */ public String getId() { return id; } /** * Gets the version number of the document in the document's modification history. * @return the version number. */ public long getVersion() { return version; } /** * Gets the time during which the document was modified. * * @return the transaction time. */ public Date getTxTime() { return txTime; } /** * Gets the transaction ID associated with this document. * * @return the transaction ID. */ public String getTxId() { return txId; } public static RevisionMetadata fromIon(final IonStruct ionStruct) { if (ionStruct == null) { throw new IllegalArgumentException("Metadata cannot be null"); } try { IonString id = (IonString) ionStruct.get("id"); IonInt version = (IonInt) ionStruct.get("version"); IonTimestamp txTime = (IonTimestamp) ionStruct.get("txTime"); IonString txId = (IonString) ionStruct.get("txId"); if (id == null || version == null || txTime == null || txId == null) { throw new IllegalArgumentException("Document is missing required fields"); } return new RevisionMetadata(id.stringValue(), version.longValue(), new Date(txTime.getMillis()), txId.stringValue()); } catch (ClassCastException e) { log.error("Failed to parse ion document"); throw new IllegalArgumentException("Document members are not of the correct type", e); } } /** * Converts a {@link RevisionMetadata} object to a string. * * @return the string representation of the {@link QldbRevision} object. */ @Override public String toString() { return "Metadata{" + "id='" + id + '\'' + ", version=" + version + ", txTime=" + txTime + ", txId='" + txId + '\'' + '}'; } /** * Check whether two {@link RevisionMetadata} objects are equivalent. * * @return {@code true} if the two objects are equal, {@code false} otherwise. */ @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } RevisionMetadata metadata = (RevisionMetadata) o; return version == metadata.version && id.equals(metadata.id) && txTime.equals(metadata.txTime) && txId.equals(metadata.txId); } /** * Generate a hash code for the {@link RevisionMetadata} object. * * @return the hash code. */ @Override public int hashCode() { // CHECKSTYLE:OFF - Disabling as we are generating a hashCode of multiple properties. return Objects.hash(id, version, txTime, txId); // CHECKSTYLE:ON } }
    5. Verifier.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.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; } }
    6. 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(); } }
    7. 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(); } }
  2. Utilice dos.java archivos (GetDigest.javayGetRevision.java) para realizar los siguientes pasos:

    • Solicite un nuevo resumen delvehicle-registration libro mayor.

    • Solicite una prueba de cada revisión de un documento de laVehicleRegistration tabla.

    • Verifique las revisiones utilizando el resumen y la prueba devueltos recalculando el resumen.

    ElGetDigest.java programa contiene el siguiente código.

    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.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; } }
    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.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

    UtilicegetDigest este método para solicitar un resumen que incluya la punta actual del diario en su libro mayor. La punta del diario hace referencia al último bloque confirmado en el momento en que QLDB recibe su solicitud.

    ElGetRevision.java programa contiene el siguiente código.

    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

    Una vez que elgetRevision método devuelva una prueba de la revisión del documento especificada, este programa utiliza una API del lado del cliente para verificar esa revisión. Para obtener información general acerca del algoritmo utilizado por esta API, consulteUsar una prueba para recalcular tu resumen.

  3. Compila y ejecuta elGetRevision.java programa para verificar criptográficamente elVehicleRegistration documento con el VIN1N4AL11D75C109151.

Para exportar y validar los datos del diario en elvehicle-registration libro mayor, continúe conPaso 8: Exportar y validar los datos del diario en un libro mayor.