체크섬 계산 - Amazon S3 Glacier

Amazon Simple Storage Service(S3)의 아카이브 스토리지를 처음 사용하는 경우, 먼저 Amazon S3의 S3 Glacier 스토리지 클래스, S3 Glacier Instant Retrieval, S3 Glacier Flexible RetrievalS3 Glacier Deep Archive에 대해 자세히 알아보는 것을 권장합니다. 자세한 내용은 Amazon S3 사용 설명서의 S3 Glacier 스토리지 클래스 및 객체 보관용 스토리지 클래스를 참조하십시오.

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

체크섬 계산

아카이브를 업로드할 때는 x-amz-sha256-tree-hashx-amz-content-sha256 헤더를 모두 포함시켜야 합니다. x-amz-sha256-tree-hash 헤더는 요청 본문에서 페이로드 체크섬을 나타냅니다. 이번 주제에서는 x-amz-sha256-tree-hash 헤더의 계산 방법에 대해서 설명하겠습니다. x-amz-content-sha256 헤더는 전체 페이로드의 해시이며, 권한을 부여하는 데 필요합니다. 자세한 정보는 스트리밍 API의 서명 계산 예제을 참조하세요.

요청 페이로드는 다음과 같습니다.

  • 전체 아카이브: 아카이브 업로드 API를 사용하여 단일 요청으로 아카이브를 업로드할 때 요청 본문에서 전체 아카이브를 전송합니다. 이때는 전체 아카이브의 체크섬이 본문에 포함되어야 합니다.

  • 아카이브 파트: 멀티파트 업로드 API를 사용하여 아카이브를 여러 파트로 나누어 업로드할 때는 요청 본문에서 아카이브 파트만 전송합니다. 이때는 아카이브 파트의 체크섬이 본문에 포함되어야 합니다. 모든 파트를 업로드한 후 멀티파트 업로드 완료 요청을 전송할 때는 전체 아카이브의 체크섬이 포함되어야 합니다.

페이로드 체크섬은 SHA256 트리-해시입니다. 트리-해시라고 불리는 이유는 체크섬을 계산하는 과정에서 SHA256 해시 값으로 이루어진 트리를 계산하기 때문입니다. 루트에 존재하는 해시 값이 전체 아카이브의 체크섬입니다.

참고

이 단원에서는 SHA256 트리-해시를 계산하는 방법에 대해서 다루지만 결과만 동일하다면 어떤 절차를 사용해도 상관없습니다.

SHA256 트리-해시의 계산 방법은 다음과 같습니다.

  1. 1MB 청크 단위마다 페이로드 데이터의 SHA256 해시를 계산합니다. 마지막 청크는 1MB보다 작을 수 있습니다. 예를 들어 3.2MB 아카이브를 업로드할 경우 처음 1MB 청크 단위 3개에 대해 각각 SHA256 해시 값을 계산한 후 나머지 0.2MB 데이터의 SHA256 해시 값을 계산합니다. 이러한 해시 값들이 트리의 리프 노드를 형성합니다.

  2. 트리에서 다음 계층을 만듭니다.

    1. 이어지는 하위 노드 해시 값 2개를 연결하고 연결된 해시 값의 SHA256 해시를 계산합니다. 이렇게 SHA256 해시를 연결 및 생성하여 하위 노드 2개에 대한 상위 노드를 만듭니다.

    2. 하위 노드가 한 개만 남게 되었을 때는 해당 해시 값을 트리의 다음 레벨로 승격합니다.

  3. 최종 트리가 루트가 될 때까지 2단계를 반복합니다. 트리의 루트는 전체 아카이브의 해시 값에 해당하며, 그에 따른 하위 트리의 루트는 멀티파트 업로드에서 각 파트의 해시 값에 해당합니다.

트리-해시 예제 1: 아카이브의 단일 요청 업로드

아카이브 업로드 API(아카이브 업로드(POST archive) 참조)를 사용하여 아카이브를 단일 요청으로 업로드할 때는 요청 페이로드에 전체 아카이브가 포함됩니다. 따라서 x-amz-sha256-tree-hash 요청 헤더에도 전체 아카이브의 트리-해시가 포함되어야 합니다. 예를 들어 6.5MB 아카이브를 업로드한다고 가정하겠습니다. 다음 다이어그램은 아카이브의 SHA256 해시를 구하는 프로세스입니다. 아카이브를 읽고 1MB 청크씩 SHA256 해시 값을 계산합니다. 나머지 0.5MB 데이터의 해시 값도 계산한 후 이전 절차에서 설명한 대로 트리를 만듭니다.


	                단일 요청으로 아카이브를 업로드하는 트리 해시 예제를 보여 주는 다이어그램입니다.

트리-해시 예제 2: 아카이브의 멀티파트 업로드

멀티파트 업로드를 사용해 아카이브를 업로드 할 경우 트리-해시의 계산 과정도 단일 요청으로 아카이브를 업로드할 때와 동일합니다. 멀티파트 업로드는 각 요청(파트 업로드(PUT uploadID) API 사용)마다 아카이브 파트만 업로드한다는 점만 유일하게 다릅니다. 따라서 각 파트에 해당하는 체크섬만 x-amz-sha256-tree-hash 요청 헤더에 입력합니다. 하지만 모든 파트를 업로드한 후에는 멀티파트 업로드 완료(POST uploadID) 요청 헤더에 전체 아카이브의 트리-해시를 입력하여 멀티파트 업로드 완료(x-amz-sha256-tree-hash 참조) 요청을 전송해야 합니다.


	                멀티파트 업로드를 사용하여 아카이브를 업로드하는 트리 해시 예제를 보여 주는 다이어그램입니다.

파일의 트리-해시 계산

여기에서 소개하는 알고리즘은 설명을 목적으로 선택되었습니다. 구현 시나리오에 따라 코드를 알맞게 최적화할 수 있습니다. 예를 들어 Amazon S3 Glacier(S3 Glacier)에 대해 Amazon SDK를 사용하여 프로그래밍하는 경우에는 트리 해시 계산이 자동으로 이루어지기 때문에 사용자는 파일 참조만 입력하면 됩니다.

예 1: Java 예제

다음은 Java를 사용하여 파일의 SHA256 트리-해시를 계산하는 방법을 나타낸 예제입니다. 이 예제는 파일 위치를 인수로 입력하여 실행하거나, 혹은 코드에서 TreeHashExample.computeSHA256TreeHash 메서드를 직접 사용할 수도 있습니다.

import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class TreeHashExample { static final int ONE_MB = 1024 * 1024; /** * Compute the Hex representation of the SHA-256 tree hash for the specified * File * * @param args * args[0]: a file to compute a SHA-256 tree hash for */ public static void main(String[] args) { if (args.length < 1) { System.err.println("Missing required filename argument"); System.exit(-1); } File inputFile = new File(args[0]); try { byte[] treeHash = computeSHA256TreeHash(inputFile); System.out.printf("SHA-256 Tree Hash = %s\n", toHex(treeHash)); } catch (IOException ioe) { System.err.format("Exception when reading from file %s: %s", inputFile, ioe.getMessage()); System.exit(-1); } catch (NoSuchAlgorithmException nsae) { System.err.format("Cannot locate MessageDigest algorithm for SHA-256: %s", nsae.getMessage()); System.exit(-1); } } /** * Computes the SHA-256 tree hash for the given file * * @param inputFile * a File to compute the SHA-256 tree hash for * @return a byte[] containing the SHA-256 tree hash * @throws IOException * Thrown if there's an issue reading the input file * @throws NoSuchAlgorithmException */ public static byte[] computeSHA256TreeHash(File inputFile) throws IOException, NoSuchAlgorithmException { byte[][] chunkSHA256Hashes = getChunkSHA256Hashes(inputFile); return computeSHA256TreeHash(chunkSHA256Hashes); } /** * Computes a SHA256 checksum for each 1 MB chunk of the input file. This * includes the checksum for the last chunk even if it is smaller than 1 MB. * * @param file * A file to compute checksums on * @return a byte[][] containing the checksums of each 1 MB chunk * @throws IOException * Thrown if there's an IOException when reading the file * @throws NoSuchAlgorithmException * Thrown if SHA-256 MessageDigest can't be found */ public static byte[][] getChunkSHA256Hashes(File file) throws IOException, NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("SHA-256"); long numChunks = file.length() / ONE_MB; if (file.length() % ONE_MB > 0) { numChunks++; } if (numChunks == 0) { return new byte[][] { md.digest() }; } byte[][] chunkSHA256Hashes = new byte[(int) numChunks][]; FileInputStream fileStream = null; try { fileStream = new FileInputStream(file); byte[] buff = new byte[ONE_MB]; int bytesRead; int idx = 0; int offset = 0; while ((bytesRead = fileStream.read(buff, offset, ONE_MB)) > 0) { md.reset(); md.update(buff, 0, bytesRead); chunkSHA256Hashes[idx++] = md.digest(); offset += bytesRead; } return chunkSHA256Hashes; } finally { if (fileStream != null) { try { fileStream.close(); } catch (IOException ioe) { System.err.printf("Exception while closing %s.\n %s", file.getName(), ioe.getMessage()); } } } } /** * Computes the SHA-256 tree hash for the passed array of 1 MB chunk * checksums. * * This method uses a pair of arrays to iteratively compute the tree hash * level by level. Each iteration takes two adjacent elements from the * previous level source array, computes the SHA-256 hash on their * concatenated value and places the result in the next level's destination * array. At the end of an iteration, the destination array becomes the * source array for the next level. * * @param chunkSHA256Hashes * An array of SHA-256 checksums * @return A byte[] containing the SHA-256 tree hash for the input chunks * @throws NoSuchAlgorithmException * Thrown if SHA-256 MessageDigest can't be found */ public static byte[] computeSHA256TreeHash(byte[][] chunkSHA256Hashes) throws NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[][] prevLvlHashes = chunkSHA256Hashes; while (prevLvlHashes.length > 1) { int len = prevLvlHashes.length / 2; if (prevLvlHashes.length % 2 != 0) { len++; } byte[][] currLvlHashes = new byte[len][]; int j = 0; for (int i = 0; i < prevLvlHashes.length; i = i + 2, j++) { // If there are at least two elements remaining if (prevLvlHashes.length - i > 1) { // Calculate a digest of the concatenated nodes md.reset(); md.update(prevLvlHashes[i]); md.update(prevLvlHashes[i + 1]); currLvlHashes[j] = md.digest(); } else { // Take care of remaining odd chunk currLvlHashes[j] = prevLvlHashes[i]; } } prevLvlHashes = currLvlHashes; } return prevLvlHashes[0]; } /** * Returns the hexadecimal representation of the input byte array * * @param data * a byte[] to convert to Hex characters * @return A String containing Hex characters */ public static String toHex(byte[] data) { StringBuilder sb = new StringBuilder(data.length * 2); for (int i = 0; i < data.length; i++) { String hex = Integer.toHexString(data[i] & 0xFF); if (hex.length() == 1) { // Append leading zero. sb.append("0"); } sb.append(hex); } return sb.toString().toLowerCase(); } }
예 2: C# .NET 예제

다음은 파일의 SHA256 트리-해시를 계산하는 방법을 나타낸 예제입니다. 이 예제는 파일 위치를 인수로 입력하여 실행할 수 있습니다.

using System; using System.IO; using System.Security.Cryptography; namespace ExampleTreeHash { class Program { static int ONE_MB = 1024 * 1024; /** * Compute the Hex representation of the SHA-256 tree hash for the * specified file * * @param args * args[0]: a file to compute a SHA-256 tree hash for */ public static void Main(string[] args) { if (args.Length < 1) { Console.WriteLine("Missing required filename argument"); Environment.Exit(-1); } FileStream inputFile = File.Open(args[0], FileMode.Open, FileAccess.Read); try { byte[] treeHash = ComputeSHA256TreeHash(inputFile); Console.WriteLine("SHA-256 Tree Hash = {0}", BitConverter.ToString(treeHash).Replace("-", "").ToLower()); Console.ReadLine(); Environment.Exit(-1); } catch (IOException ioe) { Console.WriteLine("Exception when reading from file {0}: {1}", inputFile, ioe.Message); Console.ReadLine(); Environment.Exit(-1); } catch (Exception e) { Console.WriteLine("Cannot locate MessageDigest algorithm for SHA-256: {0}", e.Message); Console.WriteLine(e.GetType()); Console.ReadLine(); Environment.Exit(-1); } Console.ReadLine(); } /** * Computes the SHA-256 tree hash for the given file * * @param inputFile * A file to compute the SHA-256 tree hash for * @return a byte[] containing the SHA-256 tree hash */ public static byte[] ComputeSHA256TreeHash(FileStream inputFile) { byte[][] chunkSHA256Hashes = GetChunkSHA256Hashes(inputFile); return ComputeSHA256TreeHash(chunkSHA256Hashes); } /** * Computes a SHA256 checksum for each 1 MB chunk of the input file. This * includes the checksum for the last chunk even if it is smaller than 1 MB. * * @param file * A file to compute checksums on * @return a byte[][] containing the checksums of each 1MB chunk */ public static byte[][] GetChunkSHA256Hashes(FileStream file) { long numChunks = file.Length / ONE_MB; if (file.Length % ONE_MB > 0) { numChunks++; } if (numChunks == 0) { return new byte[][] { CalculateSHA256Hash(null, 0) }; } byte[][] chunkSHA256Hashes = new byte[(int)numChunks][]; try { byte[] buff = new byte[ONE_MB]; int bytesRead; int idx = 0; while ((bytesRead = file.Read(buff, 0, ONE_MB)) > 0) { chunkSHA256Hashes[idx++] = CalculateSHA256Hash(buff, bytesRead); } return chunkSHA256Hashes; } finally { if (file != null) { try { file.Close(); } catch (IOException ioe) { throw ioe; } } } } /** * Computes the SHA-256 tree hash for the passed array of 1MB chunk * checksums. * * This method uses a pair of arrays to iteratively compute the tree hash * level by level. Each iteration takes two adjacent elements from the * previous level source array, computes the SHA-256 hash on their * concatenated value and places the result in the next level's destination * array. At the end of an iteration, the destination array becomes the * source array for the next level. * * @param chunkSHA256Hashes * An array of SHA-256 checksums * @return A byte[] containing the SHA-256 tree hash for the input chunks */ public static byte[] ComputeSHA256TreeHash(byte[][] chunkSHA256Hashes) { byte[][] prevLvlHashes = chunkSHA256Hashes; while (prevLvlHashes.GetLength(0) > 1) { int len = prevLvlHashes.GetLength(0) / 2; if (prevLvlHashes.GetLength(0) % 2 != 0) { len++; } byte[][] currLvlHashes = new byte[len][]; int j = 0; for (int i = 0; i < prevLvlHashes.GetLength(0); i = i + 2, j++) { // If there are at least two elements remaining if (prevLvlHashes.GetLength(0) - i > 1) { // Calculate a digest of the concatenated nodes byte[] firstPart = prevLvlHashes[i]; byte[] secondPart = prevLvlHashes[i + 1]; byte[] concatenation = new byte[firstPart.Length + secondPart.Length]; System.Buffer.BlockCopy(firstPart, 0, concatenation, 0, firstPart.Length); System.Buffer.BlockCopy(secondPart, 0, concatenation, firstPart.Length, secondPart.Length); currLvlHashes[j] = CalculateSHA256Hash(concatenation, concatenation.Length); } else { // Take care of remaining odd chunk currLvlHashes[j] = prevLvlHashes[i]; } } prevLvlHashes = currLvlHashes; } return prevLvlHashes[0]; } public static byte[] CalculateSHA256Hash(byte[] inputBytes, int count) { SHA256 sha256 = System.Security.Cryptography.SHA256.Create(); byte[] hash = sha256.ComputeHash(inputBytes, 0, count); return hash; } } }