本文属于机器翻译版本。若本译文内容与英语原文存在差异,则一律以英文原文为准。
CloudTrail 查询结果文件完整性验证的自定义实现
CloudTrail 采用了行业标准、可公开使用的加密算法和哈希函数,因此,您可以自行创建您自己的工具,以验证 CloudTrail 查询结果文件完整性。当您将查询结果保存到 Amazon S3 存储桶后,CloudTrail 会将签名文件传送到您的 S3 存储桶。您可以实施自己的验证解决方案以验证签名和查询结果文件。有关签名文件的更多信息,请参阅 CloudTrail 签名文件结构。
本主题介绍了签名文件的签名方式,并详述了实施验证签名文件及签名文件所引用查询结果文件的解决方案所需采取的步骤。
了解 CloudTrail 签名文件的签名方式
CloudTrail 签名文件使用 RSA 数字签名。对于每个签名文件,CloudTrail 将执行以下操作:
-
创建一个哈希列表,其中包含每个查询结果文件的哈希值。
-
获取区域唯一的私钥。
-
将此字符串的 SHA-256 哈希值和私钥传递给 RSA 签名算法(生成数字签名)。
-
将签名的字节代码编码成十六进制格式。
-
将数字签名放入签名文件中。
数据签名字符串的内容
数据签名字符串包含以空格分隔的每个查询结果文件的哈希值。签名文件列出了每个查询结果文件的 fileHashValue
。
自定义验证实现步骤
实施自定义验证解决方案时,需要验证签名文件及其引用的查询结果文件。
验证签名文件
要验证签名文件,您需要其签名、与用于对其进行签名的私钥对应的公钥以及您计算的数据签名字符串。
-
获取签名文件。
-
验证是否已从签名文件的原始位置检索到签名文件。
-
获取签名文件的十六进制编码签名。
-
获取与用于对签名文件进行签名的私钥对应的公钥的十六进制编码指纹。
-
检索与签名文件中的
queryCompleteTime
对应的时间范围的公钥。对于时间范围,请选择早于queryCompleteTime
的StartTime
和晚于queryCompleteTime
的EndTime
。 -
从检索到的公钥中,选择指纹与签名文件中的
publicKeyFingerprint
值匹配的公钥。 -
使用包含以空格分隔的每个查询结果文件哈希值的哈希列表,重新创建用于验证签名文件签名的数据签名字符串。签名文件列出了每个查询结果文件的
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" } ],
-
将此字符串的 SHA-256 哈希值、公钥及签名作为参数传递给 RSA 签名验证算法,以验证签名。如果结果为 true,则签名文件有效。
验证查询结果文件
如果签名文件有效,请验证签名文件引用的查询结果文件。要验证查询结果文件的完整性,请计算其压缩内容的 SHA-256 哈希值,并将结果与签名文件中记录的查询结果文件中的 fileHashValue
进行比较。如果哈希值匹配,则查询结果文件有效。
以下部分详细介绍了验证过程。
A. 获取签名文件
第一步是获取签名文件并获取公钥的指纹。
-
从 Amazon S3 存储桶中获取要验证的查询结果的签名文件。
-
接下来,从签名文件中获取
hashSignature
值。 -
在签名文件中,从
publicKeyFingerprint
字段中获取与用于对文件进行签名的私钥对应的公钥的指纹。
B. 检索用于验证签名文件的公钥
要获取用于验证签名文件的公钥,您可以使用 Amazon CLI 或 CloudTrail API。在这两种情况下,您都需要指定要验证的签名文件的时间范围(即起始时间和结束时间)。使用与签名文件中的 queryCompleteTime
对应的时间范围。对于您指定的时间范围,可能会返回一个或多个公钥。返回的密钥的有效时间范围可能会发生重叠。
注意
由于 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 对象的列表:
使用 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-keys
或 ListPublicKeys
检索到的公钥中,选择指纹与签名文件的 publicKeyFingerprint
字段中记录的指纹匹配的公钥。此即为用于验证签名文件的公钥。
D. 重新创建数据签名字符串
现在,您已拥有签名文件的签名及关联公钥,接下来,您需要计算数据签名字符串。算出数据签名字符串后,您就有了验证签名所需的输入。
数据签名字符串包含以空格分隔的每个查询结果文件的哈希值。重新创建此字符串后,您可以验证签名文件。
E. 验证签名文件
将重新创建的数据签名字符串、数字签名和公钥传递给 RSA 签名验证算法。如果输出为 true,则已验证签名文件的签名,且签名文件有效。
F. 验证查询结果文件
验证签名文件后,您可以验证其引用的查询结果文件。签名文件包含查询结果文件的 SHA-256 哈希值。如果某个查询结果文件在 CloudTrail 将其传送后发生修改,则 SHA-256 哈希值会发生变化,且签名文件的签名将不匹配。
使用以下步骤验证签名文件的 files
数组中列出的查询结果文件。
-
从签名文件中的
files.fileHashValue
字段检索文件的原始哈希值。 -
使用
hashAlgorithm
中指定的哈希算法计算压缩的查询结果文件内容的哈希值。 -
将您为每个查询结果文件生成的哈希值与签名文件中的
files.fileHashValue
进行比较。如果哈希值匹配,则查询结果文件有效。
离线验证签名和查询结果文件
离线验证签名和查询结果文件时,您通常可以按照前述部分中介绍的流程进行。但是,您必须考虑以下有关公钥的信息。
公钥
要进行离线验证,首先必须在线获取验证给定时间范围内的查询结果文件所需的公钥(例如,通过调用 ListPublicKeys
实现),然后将其离线存储。每当您需要验证超出指定的初始时间范围的其他文件时,都必须重复执行这一步。
示例验证代码段
以下示例代码段提供验证 CloudTrail 签名和查询结果文件的框架代码。此框架代码未指定在线/离线条件;也就是说,由您决定是否实现在线连接到 Amazon 的代码。建议在实现中使用 Java Cryptography Extension (JCE)
示例代码段:
-
如何创建用于验证签名文件签名的数据签名字符串。
-
如何验证签名文件的签名。
-
如何计算查询结果文件的哈希值,并将其与签名文件中列出的
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."); } }