
# 事件文件完整性校验
#### 操作场景
由于云审计采用了行业标准、可公开使用的签名算法和哈希函数，因此，您可以自行创建用于校验云审计事件文件完整性的工具。原则上进行完整性校验时必须包含字段time、service_type、resource_type、trace_name、trace_rating、trace_type，其他字段由各服务自己定义。
启用事件文件完整性校验后，云审计将摘要文件提交到您的OBS桶中，您可以使用这些文件实现自己的校验解决方案。有关摘要文件的更多信息，请参阅[摘要文件](https://support.huaweicloud.com/usermanual-cts/cts_03_0014.html)。
#### 操作前提
在进行事件文件完整性校验前，您需先了解云审计摘要文件的签名方式：
云审计摘要文件使用RSA数字签名，对于每个摘要文件，云审计执行以下操作：
1. 创建数字签名字符串（由指定摘要文件字段构成），获取RSA私钥。
2. 将数字签名字符串的哈希值和私钥传递给RSA算法，生成数字签名，将数字签名编码成十六进制格式。
3. 将该数字签名放入摘要文件对象的meta-signature元数据属性中。
 
数字签名字符串包含以下摘要文件字段：
- UTC扩展格式的摘要文件结束时间戳（2017-03-28T02-09-17Z）。
- 当前摘要文件的OBS存储路径。
- 当前摘要文件（压缩后的）的哈希值（十六进制编码）。
- 前一摘要文件的十六进制数字签名。
 
#### 校验事件文件完整性
实现事件文件完整性校验方案时，您需要先校验摘要文件，然后再校验其引用的事件文件。
1. 获取摘要文件。
   1. 从OBS桶中获取需要验证的时间范围的最新摘要文件。
   
   2. 检查该摘要文件在OBS桶中的存储位置是否与摘要文件中记录的OBS桶存储位置匹配。
   
   3. 从摘要文件对象的 meta-signature元数据属性中获取摘要文件的数字签名。
    
2. 获取用于校验数字签名的RSA公钥。 当前云审计系统的RSA公钥是
   ```
   MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsjQDkl8COPRhOCvm7ZI8sYZ20ojl+ay/gwRSk9q0gkY3pP0RrAhSsEzgYdYjaMCqixkmbpt4AH9AROJU4drnoCAZSMqRxgv0bGC9kVd4q95l4zibswAsksjuNQo/XoJjBl+rRAqCa+1uetgVU4k4Yx8RryYxYx/tImvMe/O4mGAIaTf+rsqt3VXR1QIj5lYR/nx41BEgC/Kb1elYAfDaaab8WS5INRprj7qdu6oAo4Ug47WqbecvEtG3JRpj5+oqLyW41Fvse3osC0h5DQdxTt4x00/rVZ+gH7Kua00y7gC8YOxFVpYbfn/oW61PUDeHG/N9hUjOrIgDDJpD2YbCIQIDAQAB。
   ```
   
3. 获取数字签名字符串。
   有了摘要文件的数字签名及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
   ```
   
4. 校验摘要文件。
   将[3]获取的数字签名字符串、摘要文件的数字签名和公钥传给 RSA 签名验证算法。如果输出为 true，则数字签名匹配，摘要文件有效。
   
5. 校验事件文件。
   校验摘要文件有效后，您可以校验其记录的事件文件。
   摘要文件记录了事件文件的哈希值，文件上传到OBS后会在其ETag元数据中存储该文件的哈希值，如果某个事件文件在云审计提交到OBS桶后发生修改，则其哈希值会发生变化，且摘要文件的数字签名也不匹配。
   如下是校验事件文件的具体步骤：
   1. 从摘要文件信息中获取事件文件的bucket 和object 信息。
   
   2. 调用OBS客户端接口获取事件文件对象头信息中的ETag元数据的值。
   
   3. 从摘要文件对应事件的log_hash_value字段获取事件文件的原始哈希值。
   
   4. 比较ETag元数据的值和摘要文件中事件文件的原始哈希值，如果哈希值匹配，则事件文件有效。
     
6. 校验之前的摘要文件和事件文件。
   在每个摘要文件中，如下字段提供了前一摘要文件的位置和签名：
   - previous_digest_bucket
   
   - previous_digest_object
   
   - previous_digest_signature
   
   
   按照[4]和[5]校验每个摘要文件的签名及其记录的事件文件。
   对于[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 signaturePrevious = objectPreviousMetadata.getMetadata().get("meta-signature").toString();
                   String signaturePreviousExpect = digestFile.getString("previous_digest_signature");
                   if (signaturePrevious.equals(signaturePreviousExpect)) {
                       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.");
           }
       }
   }
   ```
   
 
