CloudTrail 쿼리 결과 파일 무결성 검증에 대한 사용자 지정 구현 - AWS CloudTrail

CloudTrail 쿼리 결과 파일 무결성 검증에 대한 사용자 지정 구현

CloudTrail은 업계 표준의 공개적으로 제공되는 암호화 알고리즘 및 해시 함수를 사용하므로 고유한 도구를 생성하여 CloudTrail 쿼리 결과 파일의 무결성을 검증할 수 있습니다. 쿼리 결과를 Amazon S3 버킷에 저장하면 CloudTrail은 S3 버킷에 서명 파일을 전송합니다. 자체 검증 솔루션을 구현하여 서명과 쿼리 결과 파일을 검증할 수 있습니다. 서명 파일에 대한 자세한 내용은 CloudTrail 서명 파일 구조 단원을 참조하세요.

이 주제에서는 서명 파일이 서명되는 방법을 설명한 후 서명 파일 및 서명 파일이 참조하는 쿼리 결과 파일을 검증하는 솔루션을 구현하기 위해 수행해야 할 단계를 자세히 안내합니다.

CloudTrail 서명 파일이 서명되는 방법 이해

CloudTrail 서명 파일은 RSA 디지털 서명으로 서명됩니다. CloudTrail은 각 서명 파일에 대해 다음을 수행합니다.

  1. 각 쿼리 결과 파일의 해시 값이 포함된 해시 목록을 만듭니다.

  2. 리전에 고유한 프라이빗 키를 가져옵니다.

  3. 문자열의 SHA-256 해시 및 프라이빗 키를 RSA 서명 알고리즘에 전달하여 디지털 서명을 생성합니다.

  4. 서명의 바이트 코드를 16진수 형식으로 인코딩합니다.

  5. 디지털 서명을 서명 파일에 넣습니다.

데이터 서명 문자열의 내용

데이터 서명 문자열은 공백으로 구분된 각 쿼리 결과 파일의 해시 값으로 구성됩니다. 서명 파일에는 각 쿼리 결과 파일에 대한 fileHashValue가 나열됩니다.

사용자 지정 검증 구현 단계

사용자 지정 검증 솔루션을 구현할 때 먼저 서명 파일을 검증한 다음 서명 파일이 참조하는 쿼리 결과 파일을 검증해야 합니다.

서명 파일 검증

서명 파일을 검증하려면 파일의 서명, 파일에 서명하는 데 사용된 프라이빗 키에 대한 퍼블릭 키, 사용자가 계산한 데이터 서명 문자열이 필요합니다.

  1. 서명 파일을 가져옵니다.

  2. 서명 파일이 원래 위치에서 검색되었는지 확인합니다.

  3. 서명 파일의 16진수 인코딩 서명을 가져옵니다.

  4. 서명 파일에 서명하는 데 사용된 프라이빗 키에 대한 퍼블릭 키의 16진수 인코딩 지문을 가져옵니다.

  5. 서명 파일의 queryCompleteTime에 해당하는 시간 범위의 퍼블릭 키를 검색합니다. 시간 범위에서 StartTimequeryCompleteTime 이전의 시간으로, EndTimequeryCompleteTime 이후의 시간으로 선택합니다.

  6. 검색된 퍼블릭 키 중에서 서명 파일의 publicKeyFingerprint 값과 지문이 일치하는 퍼블릭 키를 선택합니다.

  7. 공백으로 구분된 각 쿼리 결과 파일의 해시 값을 포함하는 해시 목록을 사용하여, 서명 파일의 서명을 검증하는 데 사용되는 데이터 서명 문자열을 다시 생성합니다. 서명 파일에는 각 쿼리 결과 파일에 대한 fileHashValue가 나열됩니다.

    예를 들어, 서명 파일의 files 배열에 다음과 같은 세 개의 쿼리 결과 파일이 포함된 경우 해시 목록은 "aaa bbb ccc"입니다.

    “files": [
 {
 "fileHashValue" : “aaa”,
 "fileName" : "result_1.csv.gz"
 }, {
 "fileHashValue" : “bbb”,
 "fileName" : "result_2.csv.gz"
 }, {
 "fileHashValue" : “ccc”,
 "fileName" : "result_3.csv.gz"
 } ],
  8. 문자열의 SHA-256 해시, 퍼블릭 키 및 서명을 RSA 서명 검증 알고리즘에 파라미터로 전달하여 서명을 검증합니다. 결과가 true이면 서명 파일이 유효한 것입니다.

쿼리 결과 파일 검증

서명 파일이 유효하면 서명 파일이 참조하는 쿼리 결과 파일을 검증합니다. 쿼리 결과 파일의 무결성을 검증하려면 압축된 콘텐츠에서 해당 파일의 SHA-256 해시 값을 계산하고 그 결과를 서명 파일에 기록된 쿼리 결과 파일의 fileHashValue와 비교합니다. 해시가 서로 일치하면 쿼리 결과 파일이 유효한 것입니다.

다음 단원에서는 각 검증 프로세스를 상세히 설명합니다.

A. 서명 파일 가져오기

첫 번째 단계는 서명 파일을 가져오고 퍼블릭 키의 지문을 가져오는 것입니다.

  1. Amazon S3 버킷에서 검증하려는 쿼리 결과에 대한 서명 파일을 가져옵니다.

  2. 다음으로, 서명 파일에서 hashSignature 값을 가져옵니다.

  3. 서명 파일의 publicKeyFingerprint 필드에서 파일에 서명하는 데 사용된 프라이빗 키에 대한 퍼블릭 키의 지문을 가져옵니다.

B. 서명 파일 검증을 위한 퍼블릭 키 검색

AWS CLI 또는 CloudTrail API를 사용하여 서명 파일을 검증하기 위한 퍼블릭 키를 가져올 수 있습니다. 어느 방법을 사용하든 서명 파일에 대해 검증할 시간 범위(시작 시간 및 종료 시간)를 지정합니다. 서명 파일의 queryCompleteTime에 해당하는 시간 범위를 사용합니다. 지정한 시간 범위에 대해 하나 이상의 퍼블릭 키가 반환될 수 있습니다. 반환된 키의 유효 시간 범위가 겹칠 수 있습니다.

참고

CloudTrail은 리전별로 서로 다른 프라이빗/퍼블릭 키 페어를 사용하므로 각 서명 파일은 해당 리전에 고유한 프라이빗 키로 서명됩니다. 따라서 특정 리전의 서명 파일을 검증할 때는 동일한 리전의 퍼블릭 키를 검색해야 합니다.

AWS CLI를 사용하여 퍼블릭 키 검색

AWS CLI를 사용하여 서명 파일의 퍼블릭 키를 검색하려면 cloudtrail list-public-keys 명령을 사용합니다. 명령의 형식은 다음과 같습니다.

aws cloudtrail list-public-keys [--start-time <start-time>] [--end-time <end-time>]

시작 시간 및 종료 시간 파라미터는 UTC 타임스탬프이며 선택 사항입니다. 지정하지 않을 경우 현재 시간이 사용되며 현재 활성 상태인 퍼블릭 키가 반환됩니다.

예제 응답

응답은 반환된 키를 나타내는 JSON 객체 목록입니다.

CloudTrail API를 사용하여 퍼블릭 키 검색

CloudTrail API를 사용하여 서명 파일의 퍼블릭 키를 검색하려면 ListPublicKeys API에 시작 시간 및 종료 시간 값을 전달합니다. ListPublicKeys API는 지정된 시간 범위 내에서 서명 파일에 서명하는 데 사용된 프라이빗 키에 대한 퍼블릭 키를 반환합니다. 이 API는 각 퍼블릭 키에 대해 해당 지문도 반환합니다.

ListPublicKeys

이 단원에서는 ListPublicKeys API에 대한 요청 파라미터 및 응답 요소에 대해 설명합니다.

참고

ListPublicKeys의 이진 필드에 대한 인코딩은 변경될 수 있습니다.

요청 파라미터

이름 설명
StartTime

선택적으로 CloudTrail 서명 파일에 대한 퍼블릭 키를 검색할 시간 범위의 시작 시간(UTC)을 지정합니다. StartTime을 지정하지 않을 경우 현재 시간이 사용되며 현재 퍼블릭 키가 반환됩니다.

유형: DateTime

EndTime

선택적으로 CloudTrail 서명 파일에 대한 퍼블릭 키를 검색할 시간 범위의 끝 시간(UTC)을 지정합니다. EndTime을 지정하지 않을 경우 현재 시간이 사용됩니다.

유형: DateTime

응답 요소

PublicKeyList, 다음을 포함하는 PublicKey 객체 배열:

이름 설명
Value

PKCS #1 형식의 DER 인코딩 퍼블릭 키 값입니다.

유형: BLOB

ValidityStartTime

퍼블릭 키 유효 기간의 시작 시간입니다.

유형: DateTime

ValidityEndTime

퍼블릭 키 유효 기간의 종료 시간입니다.

유형: DateTime

Fingerprint

퍼블릭 키의 지문. 지문은 서명 파일을 검증하기 위해 사용해야 할 퍼블릭 키를 식별하는 데 사용될 수 있습니다.

유형: String

C. 검증에 사용할 퍼블릭 키 선택

list-public-keys 또는 ListPublicKeys가 검색한 퍼블릭 키 중에서 서명 파일의 publicKeyFingerprint 필드에 기록된 지문과 일치하는 지문을 가진 퍼블릭 키를 선택합니다. 이 퍼블릭 키가 서명 파일을 검증하는 데 사용할 퍼블릭 키입니다.

D. 데이터 서명 문자열 다시 생성

서명 파일의 서명 및 관련 퍼블릭 키를 구했으므로 이제 데이터 서명 문자열을 계산해야 합니다. 데이터 서명 문자열을 계산하면 서명을 검증하는 데 필요한 입력이 구해집니다.

데이터 서명 문자열은 공백으로 구분된 각 쿼리 결과 파일의 해시 값으로 구성됩니다. 이 문자열을 다시 생성한 후에 서명 파일을 검증할 수 있습니다.

E. 서명 파일 검증

다시 생성한 데이터 서명 문자열, 디지털 서명 및 퍼블릭 키를 RSA 서명 검증 알고리즘에 전달합니다. 출력이 true이면 서명 파일의 서명이 검증되었으며 서명 파일이 유효한 것입니다.

F. 쿼리 결과 파일 검증

서명 파일을 검증한 후에 서명 파일이 참조하는 쿼리 결과 파일을 검증할 수 있습니다. 서명 파일에는 쿼리 결과 파일의 SHA-256 해시가 포함되어 있습니다. CloudTrail이 쿼리 결과 파일을 전송한 후 그중 하나가 수정된 경우 SHA-256 해시가 변경되어 서명 파일의 서명이 일치하지 않게 됩니다.

다음 절차를 사용하여 서명 파일의 files 배열에 나열된 쿼리 결과 파일의 유효성을 검사합니다.

  1. 서명 파일에 있는 files.fileHashValue 필드에서 파일의 원래 해시를 검색합니다.

  2. hashAlgorithm에 지정된 해시 알고리즘을 사용하여 쿼리 결과 파일의 압축된 콘텐츠를 해시합니다.

  3. 각 쿼리 결과 파일에 대해 생성한 해시 값을 서명 파일의 files.fileHashValue와 비교합니다. 해시가 일치하면 쿼리 결과 파일이 유효한 것입니다.

서명 및 쿼리 결과 파일을 오프라인으로 검증하기

서명 및 쿼리 결과 파일을 오프라인으로 검증할 때 일반적으로 이전 단원에 설명된 절차를 따르면 됩니다. 하지만 퍼블릭 키에 대해 다음의 정보를 고려해야 합니다.

퍼블릭 키

오프라인으로 검증하려면 먼저 지정된 시간 범위에 있는 쿼리 결과 파일을 검증하는 데 필요한 모든 퍼블릭 키를 온라인으로 구한(예를 들면 ListPublicKeys 호출) 다음 오프라인에 저장해야 합니다. 처음 지정했던 시간 범위를 벗어나는 추가 파일을 검증할 때마다 이 단계를 반복해야 합니다.

검증을 위한 샘플 코드 조각

다음 샘플 코드 조각은 CloudTrail 서명 및 쿼리 결과 파일 검증을 위한 스켈레톤 코드를 제공합니다. 스켈레톤 코드는 온라인/오프라인에 무관하므로 AWS에 온라인으로 연결된 상태에서 코드를 구현할지 여부는 필요에 따라 선택하면 됩니다. 제안된 구현에서는 Java Cryptography Extension(JCE)Bouncy Castle을 보안 공급자로 사용합니다.

샘플 코드 조각은 다음을 보여 줍니다.

  • .서명 파일의 서명을 검증하는 데 사용되는 데이터 서명 문자열을 생성하는 방법.

  • 서명 파일의 서명을 검증하는 방법.

  • 쿼리 결과 파일의 해시 값을 계산하고 서명 파일에 나열된 fileHashValue 값과 비교하여 쿼리 결과 파일의 신뢰성을 확인하는 방법.

import org.apache.commons.codec.binary.Hex; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.asn1.pkcs.RSAPublicKey; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.json.JSONArray; import org.json.JSONObject; import java.security.KeyFactory; import java.security.MessageDigest; import java.security.PublicKey; import java.security.Security; import java.security.Signature; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class SignFileValidationSampleCode { public void validateSignFile(String s3Bucket, String s3PrefixPath) throws Exception { MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); // Load the sign file from S3 (using Amazon S3 Client) or from your local copy JSONObject signFile = loadSignFileToMemory(s3Bucket, String.format("%s/%s", s3PrefixPath, "result_sign.json")); // Using the Bouncy Castle provider as a JCE security provider - http://www.bouncycastle.org/ Security.addProvider(new BouncyCastleProvider()); List<String> hashList = new ArrayList<>(); JSONArray jsonArray = signFile.getJSONArray("files"); for (int i = 0; i < jsonArray.length(); i++) { JSONObject file = jsonArray.getJSONObject(i); String fileS3ObjectKey = String.format("%s/%s", s3PrefixPath, file.getString("fileName")); // Load the export file from S3 (using Amazon S3 Client) or from your local copy byte[] exportFileContent = loadCompressedExportFileInMemory(s3Bucket, fileS3ObjectKey); messageDigest.update(exportFileContent); byte[] exportFileHash = messageDigest.digest(); messageDigest.reset(); byte[] expectedHash = Hex.decodeHex(file.getString("fileHashValue")); boolean signaturesMatch = Arrays.equals(expectedHash, exportFileHash); if (!signaturesMatch) { System.err.println(String.format("Export file: %s/%s hash doesn't match.\tExpected: %s Actual: %s", s3Bucket, fileS3ObjectKey, Hex.encodeHexString(expectedHash), Hex.encodeHexString(exportFileHash))); } else { System.out.println(String.format("Export file: %s/%s hash match", s3Bucket, fileS3ObjectKey)); } hashList.add(file.getString("fileHashValue")); } String hashListString = hashList.stream().collect(Collectors.joining(" ")); /* NOTE: To find the right public key to verify the signature, call CloudTrail ListPublicKey API to get a list of public keys, then match by the publicKeyFingerprint in the sign file. Also, the public key bytes returned from ListPublicKey API are DER encoded in PKCS#1 format: PublicKeyInfo ::= SEQUENCE { algorithm AlgorithmIdentifier, PublicKey BIT STRING } AlgorithmIdentifier ::= SEQUENCE { algorithm OBJECT IDENTIFIER, parameters ANY DEFINED BY algorithm OPTIONAL } */ byte[] pkcs1PublicKeyBytes = getPublicKey(signFile.getString("queryCompleteTime"), signFile.getString("publicKeyFingerprint")); byte[] signatureContent = Hex.decodeHex(signFile.getString("hashSignature")); // Transform the PKCS#1 formatted public key to x.509 format. RSAPublicKey rsaPublicKey = RSAPublicKey.getInstance(pkcs1PublicKeyBytes); AlgorithmIdentifier rsaEncryption = new AlgorithmIdentifier(PKCSObjectIdentifiers.rsaEncryption, null); SubjectPublicKeyInfo publicKeyInfo = new SubjectPublicKeyInfo(rsaEncryption, rsaPublicKey); // Create the PublicKey object needed for the signature validation PublicKey publicKey = KeyFactory.getInstance("RSA", "BC") .generatePublic(new X509EncodedKeySpec(publicKeyInfo.getEncoded())); // Verify signature Signature signature = Signature.getInstance("SHA256withRSA", "BC"); signature.initVerify(publicKey); signature.update(hashListString.getBytes("UTF-8")); if (signature.verify(signatureContent)) { System.out.println("Sign file signature is valid."); } else { System.err.println("Sign file signature failed validation."); } System.out.println("Sign file validation completed."); } }