更新时间:2023-11-10 GMT+08:00

事件文件完整性校验

操作场景

由于云审计采用了行业标准、可公开使用的签名算法和哈希函数,因此,您可以自行创建用于校验云审计事件文件完整性的工具。原则上进行完整性校验时必须包含字段time、service_type、resource_type、trace_name、trace_rating、trace_type,其他字段由各服务自己定义。

启用事件文件完整性校验后,云审计将摘要文件提交到您的OBS桶中,您可以使用这些文件实现自己的校验解决方案。有关摘要文件的更多信息,请参阅摘要文件

操作前提

在进行事件文件完整性校验前,您需先了解云审计摘要文件的签名方式:

云审计摘要文件使用RSA数字签名,对于每个摘要文件,云审计执行以下操作:
  1. 创建数字签名字符串(由指定摘要文件字段构成),获取RSA私钥。
  2. 将数字签名字符串的哈希值和私钥传递给RSA算法,生成数字签名,将数字签名编码成十六进制格式。
  3. 将该数字签名放入摘要文件对象的meta-signature元数据属性中。
数字签名字符串包含以下摘要文件字段:
  • UTC扩展格式的摘要文件结束时间戳(2017-03-28T02-09-17Z)。
  • 当前摘要文件的OBS存储路径。
  • 当前摘要文件(压缩后的)的哈希值(十六进制编码)。
  • 前一摘要文件的十六进制数字签名。

操作步骤

实现事件文件完整性校验方案时,您需要先校验摘要文件,然后再校验其引用的事件文件。

  1. 获取摘要文件。
    1. 从华为云官网下载“OBS Java SDK”(SDK下载),调用OBS客户端接口从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/ap-southeast-1/2017/3/28/Digest/EVS/mylog_CloudTrace-Digest_ap-southeast-1_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

    按照45校验每个摘要文件的签名及其记录的事件文件。

    对于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.ap-southeast-1.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.");
            }
        }
    }