CloudTrail 日志文件完整性验证的自定义实现 - Amazon CloudTrail
Amazon Web Services 文档中描述的 Amazon Web Services 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅 中国的 Amazon Web Services 服务入门 (PDF)

本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。

CloudTrail 日志文件完整性验证的自定义实现

由于 CloudTrail 使用行业标准、公开可用的加密算法和哈希函数,因此您可以创建自己的工具来验证 CloudTrail 日志文件的完整性。启用日志文件完整性验证后,会将摘要文件 CloudTrail 传送到您的 Amazon S3 存储桶。您可以使用这些文件实现自己的验证解决方案。有关摘要文件的更多信息,请参阅CloudTrail 摘要文件结构

本主题介绍如何为摘要文件签名,然后详述了要对摘要文件及其引用的日志文件实现验证解决方案所需采取的步骤。

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

CloudTrail 摘要文件使用 RSA 数字签名进行签名。对于每个摘要文件, CloudTrail 执行以下操作:

  1. 创建一个字符串,以基于指定的摘要文件字段进行数据签名(在下一章节中讲解)。

  2. 获取区域唯一的私钥。

  3. 将此字符串的 SHA-256 哈希值和私钥传递给 RSA 签名算法(生成数字签名)。

  4. 将签名的字节代码编码成十六进制格式。

  5. 将此数字签名放入 Simple Storage Service(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 Get 或 AmazonS3Client 类(举例)从 Amazon S3 存储桶获取需要验证的时间范围的最新摘要文件。

  2. 检查用于检索此文件的 S3 存储桶和 S3 对象是否与摘要文件中记录的 S3 存储桶 S3 对象位置匹配。

  3. 接下来,从 Simple Storage Service(Amazon S3)中摘要文件对象的 x-amz-meta-signature 元数据属性获取摘要文件的数字签名。

  4. In the digest file, get the fingerprint of the public key whose private key was used to sign the digest file from the digestPublicKeyFingerprint field。

B. 检索用于验证摘要文件的公钥

要获取用于验证摘要文件的公钥,您可以使用Amazon CLI或 CloudTrail API。在这两种情况下,您都需要指定要验证的摘要文件的时间范围(即,起始时间和结束时间)。对于您指定的时间范围,可能会返回一个或多个公钥。返回的密钥的有效时间范围可能会发生重叠。

注意

由于每个区域 CloudTrail 使用不同的私钥/公钥对,因此每个摘要文件都使用其区域独有的私钥进行签名。因此,当您验证来自特定区域的摘要文件时,必须从同一区域检索其公钥。

使用 Amazon CLI 检索公钥

要使用 Amazon CLI 检索摘要文件的公钥,请使用 cloudtrail list-public-keys 命令。此命令采用以下格式:

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

start-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。ListPublicKeys API 返回与用于在指定时间范围对摘要文件进行签名的私钥对应的公钥。对于每个公钥,此 API 还返回相应的指纹。

ListPublicKeys

本部分介绍 ListPublicKeys API 的请求参数和响应元素。

注意

ListPublicKeys 的二进制字段的编码可能随时发生变化。

请求参数

名称 描述
StartTime

(可选)以 UTC 为单位指定查找 CloudTrail 摘要文件公钥的时间范围的起始时间。如果 StartTime 未指定,则使用当前时间,并返回当前的公钥。

类型: DateTime

EndTime

(可选)以 UTC 为单位指定查找 CloudTrail 摘要文件公钥的时间范围的结束时间。如果 EndTime 未指定,则使用当前时间。

类型: DateTime

响应元素

PublicKeyList - PublicKey 对象数组,包含:

名称 描述
Value

DER 编码的公钥值(采用 PKCS #1 格式)。

类型: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 S3-bucket-name/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. 使用摘要文件的 logFiles.s3BucketlogFiles.s3Object 字段中的 S3 位置信息对日志文件执行 S3 Get 操作。

  2. 如果 S3 Get 操作成功,则按照以下步骤循环访问摘要文件的 logFiles 数组中列出的日志文件:

    1. 从摘要文件中相应日志的 logFiles.hashValue 字段检索文件的原始哈希值。

    2. 使用 logFiles.hashAlgorithm 中指定的哈希算法计算未压缩的日志文件内容的哈希值。

    3. 比较您生成的哈希值和摘要文件中日志的哈希值。如果哈希值匹配,则日志文件有效。

G. 验证其他摘要和日志文件

在每个摘要文件中,以下字段提供前一个摘要文件的位置和签名:

  • previousDigestS3Bucket

  • previousDigestS3Object

  • previousDigestSignature

使用此信息顺序访问之前的摘要文件,按照前述部分中的步骤验证每个摘要文件的签名及其引用的日志文件。唯一的区别在于:对于之前的摘要文件,您不需要从摘要文件对象的 Simple Storage Service(Amazon S3)元数据属性检索数字签名。previousDigestSignature 字段提供了前一个摘要文件的签名。

您可以一直向前进行此操作,直到到达起始的摘要文件,或摘要文件链断开,以先到者为准。

离线验证摘要和日志文件

离线验证摘要和日志文件时,您通常可以按照前述部分中介绍的流程进行。但是,您必须考虑到以下方面:

处理最新的摘要文件

最新(即“当前”)摘要文件的数字签名位于摘要文件对象的 Simple Storage Service(Amazon S3)元数据属性中。在离线情况下,当前摘要文件的数字签名不可用。

对于这种情况,有两种处理方式:

  • 由于前一个摘要文件的数字签名位于当前摘要文件中,因此请从 next-to-last 摘要文件开始验证。使用这种方法时,不会验证最新的摘要文件。

  • 作为预备步骤,从摘要文件对象的元数据属性获取当前摘要文件的签名,然后将其安全地离线存储。这样,除了链中前面的文件,此最新的摘要文件也可得到验证。

路径解决方案

已下载的摘要文件中的字段(如 s3ObjectpreviousDigestS3Object)仍将指向日志文件和摘要文件的 Simple Storage Service(Amazon S3)在线位置。离线解决方案必须找到一种方法,将它们重新路由到已下载的日志和摘要文件的当前路径。

公钥

要进行离线验证,首先必须在线获取验证给定时间范围内的日志文件所需的所有公钥(例如,通过调用 ListPublicKeys 实现),然后将它们安全地离线存储。每当您需要验证超出指定的初始时间范围的其他文件时,都必须重复执行这一步。

示例验证代码段

以下示例片段提供了用于验证 CloudTrail 摘要和日志文件的基本代码。此框架代码未指定在线/离线条件;也就是说,由您决定是否实现在线连接到 Amazon 的代码。建议在实现中使用 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")); } } } }