事件文件完整性校验
操作场景
由于云审计采用了行业标准、可公开使用的签名算法和哈希函数,因此,您可以自行创建用于校验云审计事件文件完整性的工具。原则上进行完整性校验时必须包含字段time、service_type、resource_type、trace_name、trace_rating、trace_type,其他字段由各服务自己定义。
启用事件文件完整性校验后,云审计将摘要文件提交到您的OBS桶中,您可以使用这些文件实现自己的校验解决方案。有关摘要文件的更多信息,请参阅摘要文件。
操作前提
在进行事件文件完整性校验前,您需先了解云审计摘要文件的签名方式:
- 创建数字签名字符串(由指定摘要文件字段构成),获取RSA私钥。
- 将数字签名字符串的哈希值和私钥传递给RSA算法,生成数字签名,将数字签名编码成十六进制格式。
- 将该数字签名放入摘要文件对象的meta-signature元数据属性中。
- UTC扩展格式的摘要文件结束时间戳(2017-03-28T02-09-17Z)。
- 当前摘要文件的OBS存储路径。
- 当前摘要文件(压缩后的)的哈希值(十六进制编码)。
- 前一摘要文件的十六进制数字签名。
校验事件文件完整性
实现事件文件完整性校验方案时,您需要先校验摘要文件,然后再校验其引用的事件文件。
- 获取摘要文件。
- 从OBS桶中获取需要验证的时间范围的最新摘要文件。
- 检查该摘要文件在OBS桶中的存储位置是否与摘要文件中记录的OBS桶存储位置匹配。
- 从摘要文件对象的 meta-signature元数据属性中获取摘要文件的数字签名。
- 获取用于校验数字签名的RSA公钥。
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsjQDkl8COPRhOCvm7ZI8sYZ20ojl+ay/gwRSk9q0gkY3pP0RrAhSsEzgYdYjaMCqixkmbpt4AH9AROJU4drnoCAZSMqRxgv0bGC9kVd4q95l4zibswAsksjuNQo/XoJjBl+rRAqCa+1uetgVU4k4Yx8RryYxYx/tImvMe/O4mGAIaTf+rsqt3VXR1QIj5lYR/nx41BEgC/Kb1elYAfDaaab8WS5INRprj7qdu6oAo4Ug47WqbecvEtG3JRpj5+oqLyW41Fvse3osC0h5DQdxTt4x00/rVZ+gH7Kua00y7gC8YOxFVpYbfn/oW61PUDeHG/N9hUjOrIgDDJpD2YbCIQIDAQAB。
- 获取数字签名字符串。
有了摘要文件的数字签名及RSA公钥后,您需要计算数字签名字符串。计算出数字签名字符串后,您就有了验证数字签名所需的输入。
数字签名字符串采用以下格式:
signature_string = digest_end_time + digest_object + Hex(hash(digest-file-content)) + previous_digest_signature
下面是数字签名字符串的示例:
2017-03-28T02-09-17ZCloudTraces/cn-north-01/2017/3/28/Digest/EVS/mylog_CloudTrace-Digest_cn-north-01/_2017-03-28T02-09-17Z.json.gze280d203da44015e0eda3faa7a2ec9612221cc0dc8b0fe320db4febe60142350641ad19da18cb6d3f5e7faad792c3efe98836c6d6547f5e5c7a48f7088000a057af26cc3bb913cae1637befa9e4231b7d1fd6d98eaba735e509e7c5ea3c6757f732b4468f7418ef18e3312ac696dd786ec5792eacf94aee27cd7be76bf23b641c5e9a686cca6414745787254100c2bee31e584a15c2229270f9dee81f9043574
- 校验摘要文件。
将3获取的数字签名字符串、摘要文件的数字签名和公钥传给 RSA 签名验证算法。如果输出为 true,则数字签名匹配,摘要文件有效。
- 校验事件文件。
摘要文件记录了事件文件的哈希值,文件上传到OBS后会在其ETag元数据中存储该文件的哈希值,如果某个事件文件在云审计提交到OBS桶后发生修改,则其哈希值会发生变化,且摘要文件的数字签名也不匹配。
如下是校验事件文件的具体步骤:- 从摘要文件信息中获取事件文件的bucket 和object 信息。
- 调用OBS客户端接口获取事件文件对象头信息中的ETag元数据的值。
- 从摘要文件对应事件的log_hash_value字段获取事件文件的原始哈希值。
- 比较ETag元数据的值和摘要文件中事件文件的原始哈希值,如果哈希值匹配,则事件文件有效。
- 校验之前的摘要文件和事件文件。
- previous_digest_bucket
- previous_digest_object
- previous_digest_signature
对于6的摘要文件,您不需要从摘要文件对象的meta-signature元数据属性中获取数字签名。previous_digest_signature字段提供了前一摘要文件的数字签名。您可以一直向前校验摘要文件和事件文件,直到到达起始的摘要文件,或摘要文件链断开。
下面的示例代码段提供校验云审计摘要和事件文件的框架代码,该代码段使用的jar包如下,推荐使用下面jar包版本:
- esdk-obs-java-2.1.16.jar
- commons-logging-1.2.jar
- httpasyncclient-4.1.2.jar
- httpclient-4.5.3.jar
- httpcore-4.4.4.jar
- httpcore-nio-4.4.4.jar
- java-xmlbuilder-1.1.jar
- jna-4.1.0.jar
- log4j-api-2.8.2.jar
- log4j-core-2.8.2.jar
- commons-codec-1.9.jar
- json-20160810.jar
- commons-io-2.5.jar
示例校验代码段:
import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.security.KeyFactory; import java.security.MessageDigest; import java.security.PublicKey; import java.security.Signature; import java.security.spec.X509EncodedKeySpec; import java.util.Arrays; import java.util.zip.GZIPInputStream; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Hex; import org.apache.commons.io.IOUtils; import org.json.JSONObject; import com.obs.services.ObsClient; import com.obs.services.ObsConfiguration; import com.obs.services.model.ObjectMetadata; import com.obs.services.model.S3Object; public class DigestFileValidator { public static void main(String[] args) { // 摘要文件所在桶名称 String digestBucket = "bucketname"; // 摘要文件存储路径,样例:CloudTraces/eu-de/2017/11/15/Digest/ECS/tGPYa_CloudTrace-Digest_eu-de_2017-11-15T10-12-10Z.json.gz String digestObject = "digestObject"; // 认证用的ak和sk直接写到代码中有很大的安全风险,建议在配置文件或者环境变量中密文存放,使用时解密,确保安全 // 本示例以ak和sk保存在环境变量中来实现身份验证为例,运行本示例前请先在本地环境中设置环境变量HUAWEICLOUD_SDK_AK和HUAWEICLOUD_SDK_SK String ak = System.getenv("HUAWEICLOUD_SDK_AK"); String sk = System.getenv("HUAWEICLOUD_SDK_SK"); ObsConfiguration obsConfig = new ObsConfiguration(); obsConfig.setEndPoint("obs.cn-north-4.myhuaweicloud.com"); ObsClient client = new ObsClient(ak, sk, obsConfig); try { // 获取摘要文件对象 S3Object object = client.getObject(digestBucket, digestObject); InputStream is = new BufferedInputStream(object.getObjectContent()); byte[] digestFileBytes = IOUtils.toByteArray(is); // 获取摘要文件哈希值 MessageDigest messageDigest = MessageDigest.getInstance("MD5"); messageDigest.update(digestFileBytes); byte[] digestFileHashBytes = messageDigest.digest(); StringBuilder outStr = new StringBuilder(); GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(digestFileBytes)); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(gis, "UTF-8")); String line; while ((line = bufferedReader.readLine()) != null) { outStr.append(line); } bufferedReader.close(); String digestInfo = outStr.toString(); // 从OBS桶中的摘要文件头中获取元数据meta-signature的值,即该摘要文件的数字签名 ObjectMetadata objectMetadata = client.getObjectMetadata(digestBucket, digestObject); String digestSignature = objectMetadata.getMetadata().get("meta-signature").toString(); JSONObject digestFile = new JSONObject(digestInfo); // 校验摘要文件在OBS桶中是否移动过 if (!digestFile.getString("digest_bucket").equals(digestBucket) || !digestFile.getString("digest_object") .equals(digestObject)) { System.err.println("Digest file has been moved from its original location."); } else { // 获取数字签名字符串 String signatureString = digestFile.getString("digest_end_time") + digestFile.getString("digest_object") + Hex.encodeHexString(digestFileHashBytes) + digestFile.getString("previous_digest_signature"); String publicKeyString = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsjQDkl8COPRhOCvm7ZI8sYZ20ojl+ay/gwRSk9q0gkY3pP0RrAhSsEzgYdYjaMCqixkmbpt4AH9AROJU4drnoCAZSMqRxgv0bGC9kVd4q95l4zibswAsksjuNQo/XoJjBl+rRAqCa+1uetgVU4k4Yx8RryYxYx/tImvMe/O4mGAIaTf+rsqt3VXR1QIj5lYR/nx41BEgC/Kb1elYAfDaaab8WS5INRprj7qdu6oAo4Ug47WqbecvEtG3JRpj5+oqLyW41Fvse3osC0h5DQdxTt4x00/rVZ+gH7Kua00y7gC8YOxFVpYbfn/oW61PUDeHG/N9hUjOrIgDDJpD2YbCIQIDAQAB"; // 解密公钥 byte[] publicKeyBytes = Base64.decodeBase64(publicKeyString); // 构造X509EncodedKeySpec对象 X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyBytes); // 指定加密算法 KeyFactory keyFactory = KeyFactory.getInstance("RSA"); // 取公钥匙对象 PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec); Signature signatureInstance = Signature.getInstance("SHA256withRSA"); signatureInstance.initVerify(publicKey); signatureInstance.update(signatureString.getBytes("UTF-8")); byte[] signatureHashExpect = Hex.decodeHex(digestSignature.toCharArray()); // 校验签名是否有效 if (signatureInstance.verify(signatureHashExpect)) { System.out.println("Digest file signature is valid, validating log files…"); for (int i = 0; i < digestFile.getJSONArray("log_files").length(); i++) { JSONObject logFileJson = digestFile.getJSONArray("log_files").getJSONObject(i); String logBucket = logFileJson.getString("bucket"); String logObject = logFileJson.getString("object"); // 从OBS桶中的事件文件头中获取元数据ETag的值,即事件文件的哈希值 ObjectMetadata objectLogMetadata = client.getObjectMetadata(logBucket, logObject); String logHashValue = objectLogMetadata.getMetadata().get("ETag").toString(); logHashValue = logHashValue.replace("\"", ""); byte[] logFileHash = Hex.decodeHex(logHashValue.toCharArray()); // 从摘要文件中获取事件文件的哈希值 byte[] expectedHash = logFileJson.getString("log_hash_value").getBytes(); boolean hashMatch = Arrays.equals(expectedHash, logFileHash); if (!hashMatch) { System.err.println("Validate log file hash failed."); } else { System.out.println("Log file hash is valid."); } } } else { System.err.println("Validate digest signature failed."); } System.out.println("Digest file validation completed."); // 获取前一摘要文件的previous_digest_bucket, previous_digest_object, previous_digest_signature,获取到该摘要文件后校验摘要文件哈希值及数字签名 String previousDigestBucket = digestFile.getString("previous_digest_bucket"); String previousDigestObject = digestFile.getString("previous_digest_object"); // 从摘要文件对象头中的meta-signature元数据属性中获取该摘要文件的数字签名 ObjectMetadata objectPreviousMetadata = client.getObjectMetadata(previousDigestBucket, previousDigestObject); String signatruePrevious = objectPreviousMetadata.getMetadata().get("meta-signature").toString(); String signatruePreviousExpect = digestFile.getString("previous_digest_signature"); if (signatruePrevious.equals(signatruePreviousExpect)) { System.out.println( "Previous digest file signature is valid, " + "validating previous digest file hash value…"); String digestPreviousHashValue = objectPreviousMetadata.getMetadata().get("ETag").toString(); // ETag元数据的值是事件文件的哈希值用双引号引起来,这里需把双引号去掉 String digestPreviousHashValueExpect = "\"" + digestFile.getString("previous_digest_hash_value") + "\""; if (digestPreviousHashValue.equals(digestPreviousHashValueExpect)) { System.out.println("Previous digest file hash value is valid."); } else { System.err.println("Validate previous digest file hash value failed."); } } } } catch (Exception e) { System.out.println("Validate digest file failed."); } } }