本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。
CloudTrail 日志文件完整性验证的自定义实现
由于 CloudTrail 使用行业标准、公开可用的加密算法和哈希函数,因此您可以创建自己的工具来验证 CloudTrail 日志文件的完整性。启用日志文件完整性验证后,会将摘要文件 CloudTrail 传送到您的 Amazon S3 存储桶。您可以使用这些文件实现自己的验证解决方案。有关摘要文件的更多信息,请参阅CloudTrail 摘要文件结构。
本主题介绍如何为摘要文件签名,然后详述了要对摘要文件及其引用的日志文件实现验证解决方案所需采取的步骤。
了解 CloudTrail 摘要文件的签名方式
CloudTrail 摘要文件使用RSA数字签名进行签名。对于每个摘要文件,执行以下 CloudTrail操作:
-
创建一个字符串,以基于指定的摘要文件字段进行数据签名(在下一章节中讲解)。
-
获取区域唯一的私钥。
-
将字符串的 SHA -256 哈希值和私钥传递给RSA签名算法,签名算法生成数字签名。
-
将签名的字节代码编码成十六进制格式。
-
将此数字签名放入 Simple Storage Service(Amazon S3)摘要文件对象的
x-amz-meta-signature
元数据属性中。
数据签名字符串的内容
用于数据签名的字符串中包含以下 CloudTrail 对象:
-
UTC扩展格式的摘要文件的结束时间戳(例如,
2015-05-08T07:19:37Z
) -
当前摘要文件的 S3 路径
-
当前摘要文件的十六进制编码的 -256 哈希SHA值
-
之前摘要文件的十六进制编码签名
本文档的稍后部分提供了计算此字符串的格式和作为示例的字符串。
自定义验证实现步骤
实现自定义验证解决方案时,您需要先验证摘要文件,然后再验证其引用的日志文件。
验证摘要文件
要验证摘要文件,您需要其签名、与用于对其进行签名的私钥对应的公钥以及您计算的数据签名字符串。
-
获取摘要文件。
-
验证已从摘要文件的原始位置检索了摘要文件。
-
获取摘要文件的十六进制编码签名。
-
获取与用于对摘要文件进行签名的私钥对应的公钥的十六进制编码指纹。
-
检索与摘要文件对应的时间范围的公钥。
-
从检索到的公钥中,选择指纹与摘要文件中的指纹匹配的公钥。
-
使用摘要文件哈希值及其他摘要文件字段,重新创建用于验证摘要文件签名的数据签名字符串。
-
通过将字符串的 SHA -256 哈希、公钥和签名作为参数传递给签名验证算法来验证RSA签名。如果结果为 true,则摘要文件有效。
验证日志文件
如果摘要文件有效,则验证摘要文件引用的每个日志文件。
-
要验证日志文件的完整性,请计算其未压缩内容的 SHA -256 哈希值,并将结果与摘要中以十六进制记录的日志文件的哈希值进行比较。如果哈希值匹配,则日志文件有效。
-
通过使用当前摘要文件中包含的有关前一个摘要文件的信息,连续验证前一个摘要文件及其对应的日志文件。
以下部分详细介绍了这些步骤。
A. 获取摘要文件
第一步是获取最新的摘要文件,验证您已从其来源位置检索到它,然后验证其数字签名并获取公钥的指纹。
-
例如,使用 S3
GetObject
或 Amazons3Client 类,从您的 Amazon S3 存储桶中获取要验证的时间范围内的最新摘要文件。 -
检查用于检索此文件的 S3 存储桶和 S3 对象是否与摘要文件中记录的 S3 存储桶 S3 对象位置匹配。
-
接下来,从 Simple Storage Service(Amazon S3)中摘要文件对象的
x-amz-meta-signature
元数据属性获取摘要文件的数字签名。 -
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. 检索用于验证摘要文件的公钥
要获取用于验证摘要文件的公钥,您可以使用 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。ListPublicKeys
API返回在指定时间范围内使用其私钥对摘要文件进行签名的公钥。对于每个公钥,API还会返回相应的指纹。
ListPublicKeys
本节介绍的请求参数和响应元素ListPublicKeys
API。
注意
ListPublicKeys
的二进制字段的编码可能随时发生变化。
请求参数
名称 | 描述 |
---|---|
StartTime
|
(可选)在中UTC指定查找 CloudTrail 摘要文件公钥的时间范围的起始时间。如果 StartTime 未指定,则使用当前时间,并返回当前的公钥。 类型: DateTime |
EndTime
|
(可选)在中UTC指定查找 CloudTrail 摘要文件公钥的时间范围的结束时间。如果 EndTime 未指定,则使用当前时间。 类型: DateTime |
响应元素
PublicKeyList
- PublicKey
对象数组,包含:
名称 | 描述 |
Value
|
以 PKCS #1 格式DER编码的公钥值。 类型:Blob |
ValidityStartTime
|
公钥有效的起始时间。 类型: DateTime |
ValidityEndTime
|
公钥有效的结束时间。 类型: DateTime |
Fingerprint
|
公钥的指纹。指纹可用于识别验证摘要文件所必需的公钥。 类型:字符串 |
C. 选择要用于验证的公钥
从 list-public-keys
或 ListPublicKeys
返回的公钥中,选择指纹与摘要文件的 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 哈希值将发生变化,并且摘要文件的签名将不匹配。
下面的内容介绍如何验证日志文件:
-
使用摘要文件的
logFiles.s3Bucket
和logFiles.s3Object
字段中的 S3 位置信息对日志文件执行S3 Get
操作。 -
如果
S3 Get
操作成功,请使用以下步骤遍历摘要文件 logFiles 数组中列出的日志文件:-
从摘要文件中相应日志的
logFiles.hashValue
字段检索文件的原始哈希值。 -
使用
logFiles.hashAlgorithm
中指定的哈希算法计算未压缩的日志文件内容的哈希值。 -
比较您生成的哈希值和摘要文件中日志的哈希值。如果哈希值匹配,则日志文件有效。
-
G. 验证其他摘要和日志文件
在每个摘要文件中,以下字段提供前一个摘要文件的位置和签名:
-
previousDigestS3Bucket
-
previousDigestS3Object
-
previousDigestSignature
使用此信息顺序访问之前的摘要文件,按照前述部分中的步骤验证每个摘要文件的签名及其引用的日志文件。唯一的区别在于:对于之前的摘要文件,您不需要从摘要文件对象的 Simple Storage Service(Amazon S3)元数据属性检索数字签名。previousDigestSignature
字段提供了前一个摘要文件的签名。
您可以一直向前进行此操作,直到到达起始的摘要文件,或摘要文件链断开,以先到者为准。
离线验证摘要和日志文件
离线验证摘要和日志文件时,您通常可以按照前述部分中介绍的流程进行。但是,您必须考虑到以下方面:
处理最新的摘要文件
最新(即“当前”)摘要文件的数字签名位于摘要文件对象的 Simple Storage Service(Amazon S3)元数据属性中。在离线情况下,当前摘要文件的数字签名不可用。
对于这种情况,有两种处理方式:
-
由于前一个摘要文件的数字签名位于当前摘要文件中,因此请从 next-to-last 摘要文件开始验证。使用这种方法时,不会验证最新的摘要文件。
-
作为预备步骤,从摘要文件对象的元数据属性获取当前摘要文件的签名,然后将其安全地离线存储。这样,除了链中前面的文件,此最新的摘要文件也可得到验证。
路径解决方案
已下载的摘要文件中的字段(如 s3Object
和 previousDigestS3Object
)仍将指向日志文件和摘要文件的 Simple Storage Service(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")); } } } }