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 擷取摘要檔案的公開金鑰,請將開始時間和結束時間值傳遞至 ListPublicKeys API。ListPublicKeysAPI 會傳回其私有金鑰已在指定時間範圍內用來簽署摘要檔案的公有金鑰。針對每個公有金鑰,API 也會傳回對應的指紋。

ListPublicKeys

本節說明 ListPublicKeys API 的請求參數和回應元素。

注意

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

請求參數

名稱 描述
StartTime

選擇性地指定時間範圍的開始時間範圍,以查詢 CloudTrail 摘要檔案的公開金鑰。如果 StartTime 未指定,則使用當前時間,並返回當前的公鑰。

類型: DateTime

EndTime

選擇性地指定時間範圍的結束時間範圍,以查詢 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 DOC-EXAMPLE-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 Cryptography Extension (JCE)Bouncy Castle 做為安全供應商。

此範例程式碼片段說明:

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

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

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

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

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