CloudTrail 記錄檔完整性驗證的自訂實作 - AWS CloudTrail

本文為英文版的機器翻譯版本,如內容有任何歧義或不一致之處,概以英文版為準。

CloudTrail 記錄檔完整性驗證的自訂實作

由於 CloudTrail 使用業界標準、公開可用的密碼編譯演算法和雜湊函數,因此您可以建立自己的工具來驗證 CloudTrail 記錄檔的完整性。啟用日誌檔完整性驗證後,將摘要檔案 CloudTrail 交付到 Amazon S3 儲存貯體。您可以使用這些檔案來實作自己的驗證解決方案。如需摘要檔案的詳細資訊,請參閱「CloudTrail 摘要檔案結構」。

本主題說明如何簽署摘要檔案,並接著詳細說明您必須執行的步驟,以實作解決方案來驗證摘要檔案及其所參考的日誌檔案。

了解 CloudTrail 摘要文件的簽名方式

CloudTrail 摘要檔案會以RSA數位簽章簽署。對於每個摘要檔案,請 CloudTrail執行下列動作:

  1. 根據指定的摘要檔案欄位來建立資料簽署字串 (如下一節所述)。

  2. 取得區域唯一的私有金鑰。

  3. 將字串和私密金鑰的 SHA -256 雜湊傳遞至RSA簽署演算法,以產生數位簽章。

  4. 將簽章的位元組碼編碼為十六進位格式。

  5. 將數位簽章放在 Amazon S3 摘要檔案物件的 x-amz-meta-signature 中繼資料屬性。

資料簽署字串內容

下列 CloudTrail 物件包含在用於資料簽署的字串中:

  • 摘要檔案的UTC延伸格式結束時間戳記 (例如,2015-05-08T07:19:37Z)

  • 目前摘要檔案的 S3 路徑

  • 當前摘要文件的十六進制編碼 SHA -256 哈希

  • 先前摘要檔案的十六進位編碼簽章

本文件稍後會提供用於計算此字串的格式及範例字串。

自訂驗證實作步驟

實作自訂驗證解決方案時,您需要先驗證摘要檔案,再驗證其所參考的日誌檔案。

驗證摘要檔案

若要驗證摘要檔案,您需要其簽章、其私有金鑰已用來簽署的公有金鑰,以及用來運算的資料簽署字串。

  1. 取得摘要檔案。

  2. 確認已從其原始位置擷取摘要檔案。

  3. 取得摘要檔案的十六進位編碼簽章。

  4. 取得其私有金鑰已用來簽署摘要檔案之公有金鑰的十六進位編碼指紋。

  5. 擷取摘要檔案對應時間範圍內的公有金鑰。

  6. 從所擷取的公有金鑰,選擇其指紋符合摘要檔案中指紋的公有金鑰。

  7. 使用摘要檔案雜湊及其他摘要檔案欄位,重新建立用來驗證摘要檔案簽章的資料簽署字串。

  8. 將字串的 SHA -256 雜湊值、公開金鑰和簽章做為參數傳入簽章驗證演算法,以驗證RSA簽章。如果結果為 true,則摘要檔案有效。

驗證日誌檔案

如果摘要檔案有效,請驗證摘要檔案所參考的每個日誌檔案。

  1. 若要驗證記錄檔的完整性,請在未壓縮的內容上計算其 SHA -256 雜湊值,並將結果與摘要中以十六進位記錄的記錄檔雜湊進行比較。如果雜湊相符,則日誌檔案有效。

  2. 使用包含在目前摘要檔案中之先前摘要檔案的相關資訊,先驗證先前摘要檔案,再驗證其對應的日誌檔案。

下列各節將詳細說明這些步驟。

A. 取得摘要檔案

第一個步驟是取得最新摘要檔案、確認您已從其原始位置擷取該檔案、確認其數位簽章,並取得公有金鑰的指紋。

  1. 使用 S3 GetObject或 Amazons3 用戶端類別 (例如),從 Amazon S3 儲存貯體取得您想要驗證的時間範圍內的最新摘要檔案。

  2. 確認用來擷取檔案的 S3 儲存貯體和 S3 物件符合摘要檔案本身所記錄的 S3 儲存貯體 S3 物件位置。

  3. 接下來,從 Amazon S3 中摘要檔案物件的 x-amz-meta-signature 中繼資料屬性,取得摘要檔案的數位簽章。

  4. 在摘要檔案中,從 digestPublicKeyFingerprint 欄位取得其私有金鑰已用來簽署摘要檔案之公有金鑰的指紋。

B. 擷取用於驗證摘要檔案的公有金鑰

若要取得公開金鑰來驗證摘要檔案,您可以使用 AWS CLI 或 CloudTrail API. 在這兩種情況下,您會為要驗證的摘要檔案指定時間範圍 (即開始時間和結束時間)。您指定的時間範圍內可能會傳回一或多個公有金鑰。傳回的金鑰可能會有重疊的有效時間範圍。

注意

由於每個區域 CloudTrail 使用不同的私鑰/公鑰對,因此每個摘要文件都使用其區域專有的私鑰進行簽名。因此,當您驗證來自特定區域的摘要檔案時,您必須從同一個區域擷取其公有金鑰。

使用擷 AWS CLI 取公開金鑰

若要使用擷取摘要檔案的公開金鑰 AWS CLI,請使用cloudtrail list-public-keys指令。此命令的格式如下:

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

開始時間和結束時間參數是時間UTC戳記,並且是可選的。如果未指定,則會使用目前的時間,並傳回一或多個目前作用中的公有金鑰。

回應範例

響應將是代表返回的鍵(或鍵)的JSON對象列表:

{ "publicKeyList": [ { "ValidityStartTime": "1436317441.0", "ValidityEndTime": "1438909441.0", "Value": "MIIBCgKCAQEAn11L2YZ9h7onug2ILi1MWyHiMRsTQjfWE+pHVRLk1QjfWhirG+lpOa8NrwQ/r7Ah5bNL6HepznOU9XTDSfmmnP97mqyc7z/upfZdS/AHhYcGaz7n6Wc/RRBU6VmiPCrAUojuSk6/GjvA8iOPFsYDuBtviXarvuLPlrT9kAd4Lb+rFfR5peEgBEkhlzc5HuWO7S0y+KunqxX6jQBnXGMtxmPBPP0FylgWGNdFtks/4YSKcgqwH0YDcawP9GGGDAeCIqPWIXDLG1jOjRRzWfCmD0iJUkz8vTsn4hq/5ZxRFE7UBAUiVcGbdnDdvVfhF9C3dQiDq3k7adQIziLT0cShgQIDAQAB", "Fingerprint": "8eba5db5bea9b640d1c96a77256fe7f2" }, { "ValidityStartTime": "1434589460.0", "ValidityEndTime": "1437181460.0", "Value": "MIIBCgKCAQEApfYL2FiZhpN74LNWVUzhR+VheYhwhYm8w0n5Gf6i95ylW5kBAWKVEmnAQG7BvS5g9SMqFDQx52fW7NWV44IvfJ2xGXT+wT+DgR6ZQ+6yxskQNqV5YcXj4Aa5Zz4jJfsYjDuO2MDTZNIzNvBNzaBJ+r2WIWAJ/Xq54kyF63B6WE38vKuDE7nSd1FqQuEoNBFLPInvgggYe2Ym1Refe2z71wNcJ2kY+q0h1BSHrSM8RWuJIw7MXwF9iQncg9jYzUlNJomozQzAG5wSRfbplcCYNY40xvGd/aAmO0m+Y+XFMrKwtLCwseHPvj843qVno6x4BJN9bpWnoPo9sdsbGoiK3QIDAQAB", "Fingerprint": "8933b39ddc64d26d8e14ffbf6566fee4" }, { "ValidityStartTime": "1434589370.0", "ValidityEndTime": "1437181370.0", "Value": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqlzPJbvZJ42UdcmLfPUqXYNfOs6I8lCfao/tOs8CmzPOEdtLWugB9xoIUz78qVHdKIqxbaG4jWHfJBiOSSFBM0lt8cdVo4TnRa7oG9io5pysS6DJhBBAeXsicufsiFJR+wrUNh8RSLxL4k6G1+BhLX20tJkZ/erT97tDGBujAelqseGg3vPZbTx9SMfOLN65PdLFudLP7Gat0Z9p5jw/rjpclKfo9Bfc3heeBxWGKwBBOKnFAaN9V57pOaosCvPKmHd9bg7jsQkI9Xp22IzGLsTFJZYVA3KiTAElDMu80iFXPHEq9hKNbt9e4URFam+1utKVEiLkR2disdCmPTK0VQIDAQAB", "Fingerprint": "31e8b5433410dfb61a9dc45cc65b22ff" } ] }

使用 CloudTrail API 擷取公有金鑰

若要使用擷取摘要檔案的公開金鑰 CloudTrail API,請將開始時間和結束時間值傳遞給 ListPublicKeysAPI. 會ListPublicKeysAPI傳回私密金鑰在指定時間範圍內用來簽署摘要檔案的公開金鑰。對於每個公開金鑰,API也會傳回對應的指紋。

ListPublicKeys

本節說明的要求參數和回應元素ListPublicKeysAPI。

注意

ListPublicKeys 的二進位欄位編碼可能會有所變更。

請求參數

名稱 描述
StartTime

(可選) 在中UTC指定要查詢 CloudTrail 摘要檔案公開金鑰的時間範圍的開始時間範圍。如果 StartTime 未指定,則使用當前時間,並返回當前的公鑰。

類型: DateTime

EndTime

(可選) 在中UTC指定查詢 CloudTrail 摘要檔案公開金鑰的時間範圍結束時間範圍。如果 EndTime 未指定,則使用目前時間。

類型: DateTime

回應元素

PublicKeyListPublicKey 物件陣列,其中包含:

名稱 描述
Value

以 PKCS #1 格式DER編碼的公開金鑰值。

類型:Blob

ValidityStartTime

公有金鑰的有效開始時間。

類型: DateTime

ValidityEndTime

公有金鑰的有效結束時間。

類型: DateTime

Fingerprint

公有金鑰的指紋。該指紋可用來識別驗證摘要檔案所需使用的公有金鑰。

類型:字串

C. 選擇要用於驗證的公有金鑰

list-public-keysListPublicKeys 所擷取的公有金鑰,選擇其指紋符合摘要檔案 digestPublicKeyFingerprint 欄位中所記錄之指紋的公有金鑰。這是您將用來驗證摘要檔案的公有金鑰。

D. 重新建立資料簽署字串

現在您已擁有摘要檔案的簽章及相關聯的公有金鑰,您需要計算資料簽署字串。計算資料簽署字串之後,您將擁有驗證簽章所需的輸入。

資料簽署字串的格式如下:

Data_To_Sign_String = Digest_End_Timestamp_in_UTC_Extended_format + '\n' + Current_Digest_File_S3_Path + '\n' + Hex(Sha256(current-digest-file-content)) + '\n' + Previous_digest_signature_in_hex

Data_To_Sign_String 範例如下。

2015-08-12T04:01:31Z amzn-s3-demo-bucket/AWSLogs/111122223333/CloudTrail-Digest/us-east-2/2015/08/12/111122223333_us-east-2_CloudTrail-Digest_us-east-2_20150812T040131Z.json.gz 4ff08d7c6ecd6eb313257e839645d20363ee3784a2328a7d76b99b53cc9bcacd 6e8540b83c3ac86a0312d971a225361d28ed0af20d70c211a2d405e32abf529a8145c2966e3bb47362383a52441545ed091fb81 d4c7c09dd152b84e79099ce7a9ec35d2b264eb92eb6e090f1e5ec5d40ec8a0729c02ff57f9e30d5343a8591638f8b794972ce15bb3063a01972 98b0aee2c1c8af74ec620261529265e83a9834ebef6054979d3e9a6767dfa6fdb4ae153436c567d6ae208f988047ccfc8e5e41f7d0121e54ed66b1b904f80fb2ce304458a2a6b91685b699434b946c52589e9438f8ebe5a0d80522b2f043b3710b87d2cda43e5c1e0db921d8d540b9ad5f6d4$31b1f4a8ef2d758424329583897339493a082bb36e782143ee5464b4e3eb4ef6

重新建立此字串之後,您可以驗證摘要檔案。

E. 驗證摘要檔案

將重新建立的資料簽章字串、數位簽章和公開金鑰的 SHA -256 雜湊傳遞至RSA簽章驗證演算法。如果輸出為 true,則摘要檔案的簽章已經過驗證且摘要檔案有效。

F. 驗證日誌檔案

驗證摘要檔案之後,您可以驗證其所參考的日誌檔案。摘要檔包含記錄檔的 SHA -256 雜湊。如果其中一個記錄檔在 CloudTrail 傳送之後被修改,SHA-256 雜湊將會變更,且摘要檔的簽章將不符。

以下說明如何驗證日誌檔案:

  1. 使用摘要檔案之 S3 GetlogFiles.s3Bucket 欄位中的 S3 位置資訊,對日誌檔案執行 logFiles.s3Object

  2. 如果S3 Get作業成功,請使用下列步驟重複摘要檔 logFiles 陣列中列出的記錄檔:

    1. 從摘要檔案中對應日誌的 logFiles.hashValue 欄位,擷取檔案的原始雜湊。

    2. 使用 logFiles.hashAlgorithm 中指定的雜湊演算法,將日誌檔案的未壓縮內容進行雜湊。

    3. 將所產生的雜湊值與摘要檔案中日誌的雜湊值進行比較。如果雜湊相符,則日誌檔案有效。

G. 驗證其他摘要和日誌檔案

在每個摘要檔案中,下列欄位提供先前摘要檔案的位置和簽章:

  • previousDigestS3Bucket

  • previousDigestS3Object

  • previousDigestSignature

使用這項資訊循序瀏覽先前摘要檔案,並使用先前章節中的步驟來驗證每個摘要檔案的簽章及其所參考的日誌檔案。唯一的差別是,針對先前的摘要檔案,您不需要從摘要檔案物件的 Amazon S3 中繼資料屬性擷取數位簽章。previousDigestSignature 欄位中會為您提供先前摘要檔案的簽章。

您可以回到摘要檔案一開始,或直到摘要檔案鏈結中斷為止,以兩者之中先到者為準。

離線驗證摘要和日誌檔案

離線驗證摘要和日誌檔案時,您通常可以遵循先前章節中所述的程序。不過,您必須考慮下列幾個部分:

處理最新摘要檔案

最新 (即「目前」) 摘要檔案的數位簽章位在摘要檔案物件的 Amazon S3 中繼資料屬性中。在離線案例中,目前摘要檔案的數位簽章將無法使用。

有兩個方法可處理此問題:

  • 由於上一個摘要檔案的數位簽章位於目前摘要檔案中,因此請從 next-to-last 摘要檔案開始驗證。使用此方法,就不會驗證最新摘要檔案。

  • 在所有步驟之前,先從摘要檔案物件的中繼資料屬性取得目前摘要檔案的簽章,然號將它存放在安全的離線位置。如此除了鏈結中的先前檔案,還能驗證目前摘要檔案。

路徑解析

已下載摘要檔案中的欄位 (例如 s3ObjectpreviousDigestS3Object) 仍會指向日誌檔案和摘要檔案的 Amazon S3 線上位置。離線解決方案必須設法將這些位置,重新路由到已下載日誌和摘要檔案的目前路徑。

公有金鑰

若要離線驗證,您必須先在線上取得在指定時間範圍內驗證日誌檔案所需的所有公有金鑰 (例如藉由呼叫 ListPublicKeys),然後存放在安全的離線位置。每當您想要在指定的初始時間範圍外驗證其他檔案時,都必須重複此步驟。

範例驗證程式碼片段

下列範例程式碼片段提供了用於驗證 CloudTrail 摘要和記錄檔的基礎架構程式碼。此骨架程式碼線上/離線皆可使用;也就是說,您可以決定是否要線上連線到 AWS來實作它。建議的實現使用 Java 密碼學擴展(JCE)充氣城堡作為安全提供程序。

此範例程式碼片段說明:

  • 如何建立用來驗證摘要檔案簽章的資料簽署字串。

  • 如何驗證摘要檔案簽章。

  • 如何驗證日誌檔案雜湊。

  • 驗證摘要檔案鏈結的程式碼結構。

import java.util.Arrays; import java.security.MessageDigest; import java.security.KeyFactory; import java.security.PublicKey; import java.security.Security; import java.security.Signature; import java.security.spec.X509EncodedKeySpec; import org.json.JSONObject; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.apache.commons.codec.binary.Hex; public class DigestFileValidator { public void validateDigestFile(String digestS3Bucket, String digestS3Object, String digestSignature) { // Using the Bouncy Castle provider as a JCE security provider - http://www.bouncycastle.org/ Security.addProvider(new BouncyCastleProvider()); // Load the digest file from S3 (using Amazon S3 Client) or from your local copy JSONObject digestFile = loadDigestFileInMemory(digestS3Bucket, digestS3Object); // Check that the digest file has been retrieved from its original location if (!digestFile.getString("digestS3Bucket").equals(digestS3Bucket) || !digestFile.getString("digestS3Object").equals(digestS3Object)) { System.err.println("Digest file has been moved from its original location."); } else { // Compute digest file hash MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); messageDigest.update(convertToByteArray(digestFile)); byte[] digestFileHash = messageDigest.digest(); messageDigest.reset(); // Compute the data to sign String dataToSign = String.format("%s%n%s/%s%n%s%n%s", digestFile.getString("digestEndTime"), digestFile.getString("digestS3Bucket"), digestFile.getString("digestS3Object"), // Constructing the S3 path of the digest file as part of the data to sign Hex.encodeHexString(digestFileHash), digestFile.getString("previousDigestSignature")); byte[] signatureContent = Hex.decodeHex(digestSignature); /* 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 digest 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 } */ pkcs1PublicKeyBytes = getPublicKey(digestFile.getString("digestPublicKeyFingerprint"))); // 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(dataToSign.getBytes("UTF-8")); if (signature.verify(signatureContent)) { System.out.println("Digest file signature is valid, validating log files…"); for (int i = 0; i < digestFile.getJSONArray("logFiles").length(); i++) { JSONObject logFileMetadata = digestFile.getJSONArray("logFiles").getJSONObject(i); // Compute log file hash byte[] logFileContent = loadUncompressedLogFileInMemory( logFileMetadata.getString("s3Bucket"), logFileMetadata.getString("s3Object") ); messageDigest.update(logFileContent); byte[] logFileHash = messageDigest.digest(); messageDigest.reset(); // Retrieve expected hash for the log file being processed byte[] expectedHash = Hex.decodeHex(logFileMetadata.getString("hashValue")); boolean signaturesMatch = Arrays.equals(expectedHash, logFileHash); if (!signaturesMatch) { System.err.println(String.format("Log file: %s/%s hash doesn't match.\tExpected: %s Actual: %s", logFileMetadata.getString("s3Bucket"), logFileMetadata.getString("s3Object"), Hex.encodeHexString(expectedHash), Hex.encodeHexString(logFileHash))); } else { System.out.println(String.format("Log file: %s/%s hash match", logFileMetadata.getString("s3Bucket"), logFileMetadata.getString("s3Object"))); } } } else { System.err.println("Digest signature failed validation."); } System.out.println("Digest file validation completed."); if (chainValidationIsEnabled()) { // This enables the digests' chain validation validateDigestFile( digestFile.getString("previousDigestS3Bucket"), digestFile.getString("previousDigestS3Object"), digestFile.getString("previousDigestSignature")); } } } }