Files
fullstack-starter/docs/工具箱端-授权对接指南/工具箱端-报告加密与签名生成指南.md

21 KiB
Raw Blame History

工具箱端 - 报告加密与签名生成指南

概述

本文档说明工具箱端如何生成加密和签名的检查报告 ZIP 文件,以确保:

  1. 授权校验:只有合法授权的工具箱才能生成有效的报告
  2. 防篡改校验:确保报告内容在传输过程中未被篡改

UX 集成模式补充(当前项目实现)

在当前集成模式中,工具箱可将原始报告 ZIP 直接上传到 UX 的 crypto.signAndPackReport

  1. UX 校验 ZIP 并提取必需文件;
  2. UX 生成 deviceSignaturesummary.jsonMETA-INF/manifest.jsonMETA-INF/signature.asc
  3. UX 重新打包并返回签名后的 ZIPBase64工具箱再用于离线介质回传平台。

一、ZIP 文件结构要求

工具箱生成的 ZIP 文件必须包含以下文件:

report.zip
├── summary.json              # 摘要信息(必须包含授权和签名字段)
├── assets.json               # 资产信息(用于签名校验)
├── vulnerabilities.json      # 漏洞信息(用于签名校验)
├── weakPasswords.json        # 弱密码信息(用于签名校验)
├── 漏洞评估报告.html         # 漏洞评估报告(用于签名校验)
└── META-INF/
    ├── manifest.json         # 文件清单(用于 OpenPGP 签名)
    └── signature.asc         # OpenPGP 签名文件(防篡改)

二、授权校验 - 设备签名device_signature

2.1 目的

设备签名用于验证报告是由合法授权的工具箱生成的,防止第三方伪造扫描结果。

2.2 密钥派生

使用 HKDF-SHA256 从设备的 licencefingerprint 派生签名密钥:

K = HKDF(
    input = licence + fingerprint,        # 输入密钥材料(字符串拼接)
    salt = "AUTH_V3_SALT",                # 固定盐值
    info = "device_report_signature",     # 固定信息参数
    hash = SHA-256,                       # 哈希算法
    length = 32                           # 输出密钥长度32字节 = 256位
)

伪代码示例

import hkdf

# 输入密钥材料
ikm = licence + fingerprint  # 字符串直接拼接

# HKDF 参数
salt = "AUTH_V3_SALT"
info = "device_report_signature"
key_length = 32  # 32字节 = 256位

# 派生密钥
derived_key = hkdf.HKDF(
    algorithm=hashlib.sha256,
    length=key_length,
    salt=salt.encode('utf-8'),
    info=info.encode('utf-8'),
    ikm=ikm.encode('utf-8')
).derive()

2.3 签名数据组装(严格顺序)

签名数据必须按照以下严格顺序组装:

sign_payload = 
    taskId +                                    # 任务ID字符串
    inspectionId +                              # 检查ID数字转字符串
    SHA256(assets.json) +                      # assets.json 的 SHA256hex字符串小写
    SHA256(vulnerabilities.json) +             # vulnerabilities.json 的 SHA256hex字符串小写
    SHA256(weakPasswords.json) +               # weakPasswords.json 的 SHA256hex字符串小写
    SHA256(漏洞评估报告.html)                   # 漏洞评估报告.html 的 SHA256hex字符串小写

重要说明

  • 所有字符串直接拼接,不添加任何分隔符
  • SHA256 哈希值必须是 hex 字符串(小写),例如:a1b2c3d4...
  • 文件内容必须是原始字节,不能进行任何编码转换
  • 顺序必须严格一致,任何顺序错误都会导致签名验证失败

伪代码示例

import hashlib

# 1. 读取文件内容(原始字节)
assets_content = read_file("assets.json")
vulnerabilities_content = read_file("vulnerabilities.json")
weak_passwords_content = read_file("weakPasswords.json")
report_html_content = read_file("漏洞评估报告.html")

# 2. 计算 SHA256hex字符串小写
def sha256_hex(content: bytes) -> str:
    return hashlib.sha256(content).hexdigest()

assets_sha256 = sha256_hex(assets_content)
vulnerabilities_sha256 = sha256_hex(vulnerabilities_content)
weak_passwords_sha256 = sha256_hex(weak_passwords_content)
report_html_sha256 = sha256_hex(report_html_content)

# 3. 组装签名数据(严格顺序,直接拼接)
sign_payload = (
    str(task_id) +
    str(inspection_id) +
    assets_sha256 +
    vulnerabilities_sha256 +
    weak_passwords_sha256 +
    report_html_sha256
)

2.4 生成设备签名

使用 HMAC-SHA256 计算签名:

device_signature = Base64(HMAC-SHA256(key=K, data=sign_payload))

伪代码示例

import hmac
import base64

# 使用派生密钥计算 HMAC-SHA256
mac = hmac.new(
    key=derived_key,                    # 派生密钥32字节
    msg=sign_payload.encode('utf-8'),  # 签名数据UTF-8编码
    digestmod=hashlib.sha256
)

# 计算签名
signature_bytes = mac.digest()

# Base64 编码
device_signature = base64.b64encode(signature_bytes).decode('utf-8')

2.5 写入 summary.json

device_signature 写入 summary.json

{
  "orgId": 1173040813421105152,
  "checkId": 702286470691215417,
  "taskId": "TASK-20260115-4875",
  "licence": "LIC-8F2A-XXXX",
  "fingerprint": "FP-2c91e9f3",
  "deviceSignature": "Base64编码的签名值",
  "summary": "检查摘要信息",
  ...其他字段...
}

必需字段

  • licence:设备授权码(字符串)
  • fingerprint:设备硬件指纹(字符串)
  • taskId任务ID字符串
  • deviceSignature设备签名Base64字符串
  • checkIdinspectionId检查ID数字

三、防篡改校验 - OpenPGP 签名

3.1 目的

OpenPGP 签名用于验证 ZIP 文件在传输过程中未被篡改,确保文件完整性。

3.2 生成 manifest.json

创建 META-INF/manifest.json 文件,包含所有文件的 SHA-256 哈希值:

{
  "files": {
    "summary.json": "a1b2c3d4e5f6...",
    "assets.json": "b2c3d4e5f6a1...",
    "vulnerabilities.json": "c3d4e5f6a1b2...",
    "weakPasswords.json": "d4e5f6a1b2c3...",
    "漏洞评估报告.html": "e5f6a1b2c3d4..."
  }
}

伪代码示例

import hashlib
import json

def calculate_sha256_hex(content: bytes) -> str:
    return hashlib.sha256(content).hexdigest()

# 计算所有文件的 SHA256
files_hashes = {
    "summary.json": calculate_sha256_hex(summary_content),
    "assets.json": calculate_sha256_hex(assets_content),
    "vulnerabilities.json": calculate_sha256_hex(vulnerabilities_content),
    "weakPasswords.json": calculate_sha256_hex(weak_passwords_content),
    "漏洞评估报告.html": calculate_sha256_hex(report_html_content)
}

# 生成 manifest.json
manifest = {
    "files": files_hashes
}

manifest_json = json.dumps(manifest, ensure_ascii=False, indent=2)

3.3 生成 OpenPGP 签名

使用工具箱的私钥manifest.json 进行 OpenPGP 签名,生成 META-INF/signature.asc

伪代码示例(使用 Python gnupg

import gnupg

# 初始化 GPG
gpg = gnupg.GPG()

# 导入私钥(或使用已配置的密钥)
# gpg.import_keys(private_key_data)

# 对 manifest.json 进行签名
with open('META-INF/manifest.json', 'rb') as f:
    signed_data = gpg.sign_file(
        f,
        detach=True,           # 分离式签名
        clearsign=False,      # 不使用明文签名
        output='META-INF/signature.asc'
    )

伪代码示例(使用 BouncyCastle - Java/Kotlin

import org.bouncycastle.openpgp.*
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPPrivateKey
import java.io.ByteArrayOutputStream
import java.io.FileOutputStream

fun generatePGPSignature(
    manifestContent: ByteArray,
    privateKey: PGPPrivateKey,
    publicKey: PGPPublicKey
): ByteArray {
    val signatureGenerator = PGPSignatureGenerator(
        JcaPGPContentSignerBuilder(publicKey.algorithm, PGPUtil.SHA256)
    )
    signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey)
    signatureGenerator.update(manifestContent)
    
    val signature = signatureGenerator.generate()
    val signatureList = PGPSignatureList(signature)
    
    val out = ByteArrayOutputStream()
    val pgpOut = PGPObjectFactory(PGPUtil.getEncoderStream(out))
    signatureList.encode(out)
    
    return out.toByteArray()
}

3.4 打包 ZIP 文件

将所有文件打包成 ZIP 文件,确保包含:

  • 所有报告文件summary.json、assets.json 等)
  • META-INF/manifest.json
  • META-INF/signature.asc

伪代码示例

import zipfile

def create_signed_zip(output_path: str):
    with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
        # 添加报告文件
        zipf.write('summary.json', 'summary.json')
        zipf.write('assets.json', 'assets.json')
        zipf.write('vulnerabilities.json', 'vulnerabilities.json')
        zipf.write('weakPasswords.json', 'weakPasswords.json')
        zipf.write('漏洞评估报告.html', '漏洞评估报告.html')
        
        # 添加签名文件
        zipf.write('META-INF/manifest.json', 'META-INF/manifest.json')
        zipf.write('META-INF/signature.asc', 'META-INF/signature.asc')

四、完整流程示例

4.1 Python 完整示例

import hashlib
import hmac
import base64
import json
import zipfile
import hkdf
import gnupg

def generate_report_zip(
    licence: str,
    fingerprint: str,
    task_id: str,
    inspection_id: int,
    output_path: str
):
    """
    生成带签名和加密的检查报告 ZIP 文件
    """
    
    # ========== 1. 读取报告文件 ==========
    assets_content = read_file("assets.json")
    vulnerabilities_content = read_file("vulnerabilities.json")
    weak_passwords_content = read_file("weakPasswords.json")
    report_html_content = read_file("漏洞评估报告.html")
    
    # ========== 2. 生成设备签名 ==========
    
    # 2.1 密钥派生
    ikm = licence + fingerprint
    salt = "AUTH_V3_SALT"
    info = "device_report_signature"
    key_length = 32
    
    derived_key = hkdf.HKDF(
        algorithm=hashlib.sha256,
        length=key_length,
        salt=salt.encode('utf-8'),
        info=info.encode('utf-8'),
        ikm=ikm.encode('utf-8')
    ).derive()
    
    # 2.2 计算文件 SHA256
    def sha256_hex(content: bytes) -> str:
        return hashlib.sha256(content).hexdigest()
    
    assets_sha256 = sha256_hex(assets_content)
    vulnerabilities_sha256 = sha256_hex(vulnerabilities_content)
    weak_passwords_sha256 = sha256_hex(weak_passwords_content)
    report_html_sha256 = sha256_hex(report_html_content)
    
    # 2.3 组装签名数据(严格顺序)
    sign_payload = (
        str(task_id) +
        str(inspection_id) +
        assets_sha256 +
        vulnerabilities_sha256 +
        weak_passwords_sha256 +
        report_html_sha256
    )
    
    # 2.4 计算 HMAC-SHA256
    mac = hmac.new(
        key=derived_key,
        msg=sign_payload.encode('utf-8'),
        digestmod=hashlib.sha256
    )
    device_signature = base64.b64encode(mac.digest()).decode('utf-8')
    
    # 2.5 生成 summary.json
    summary = {
        "orgId": 1173040813421105152,
        "checkId": inspection_id,
        "taskId": task_id,
        "licence": licence,
        "fingerprint": fingerprint,
        "deviceSignature": device_signature,
        "summary": "检查摘要信息"
    }
    summary_content = json.dumps(summary, ensure_ascii=False).encode('utf-8')
    
    # ========== 3. 生成 OpenPGP 签名 ==========
    
    # 3.1 生成 manifest.json
    files_hashes = {
        "summary.json": sha256_hex(summary_content),
        "assets.json": assets_sha256,
        "vulnerabilities.json": vulnerabilities_sha256,
        "weakPasswords.json": weak_passwords_sha256,
        "漏洞评估报告.html": report_html_sha256
    }
    manifest = {"files": files_hashes}
    manifest_content = json.dumps(manifest, ensure_ascii=False, indent=2).encode('utf-8')
    
    # 3.2 生成 OpenPGP 签名
    gpg = gnupg.GPG()
    with open('META-INF/manifest.json', 'wb') as f:
        f.write(manifest_content)
    
    with open('META-INF/manifest.json', 'rb') as f:
        signed_data = gpg.sign_file(
            f,
            detach=True,
            output='META-INF/signature.asc'
        )
    
    # ========== 4. 打包 ZIP 文件 ==========
    with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
        zipf.writestr('summary.json', summary_content)
        zipf.writestr('assets.json', assets_content)
        zipf.writestr('vulnerabilities.json', vulnerabilities_content)
        zipf.writestr('weakPasswords.json', weak_passwords_content)
        zipf.writestr('漏洞评估报告.html', report_html_content)
        zipf.writestr('META-INF/manifest.json', manifest_content)
        zipf.write('META-INF/signature.asc', 'META-INF/signature.asc')
    
    print(f"报告 ZIP 文件生成成功: {output_path}")

4.2 Java/Kotlin 完整示例

import org.bouncycastle.crypto.digests.SHA256Digest
import org.bouncycastle.crypto.generators.HKDFBytesGenerator
import org.bouncycastle.crypto.params.HKDFParameters
import java.security.MessageDigest
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import java.util.Base64
import java.util.zip.ZipOutputStream
import java.io.FileOutputStream

fun generateReportZip(
    licence: String,
    fingerprint: String,
    taskId: String,
    inspectionId: Long,
    outputPath: String
) {
    // ========== 1. 读取报告文件 ==========
    val assetsContent = readFile("assets.json")
    val vulnerabilitiesContent = readFile("vulnerabilities.json")
    val weakPasswordsContent = readFile("weakPasswords.json")
    val reportHtmlContent = readFile("漏洞评估报告.html")
    
    // ========== 2. 生成设备签名 ==========
    
    // 2.1 密钥派生
    val ikm = (licence + fingerprint).toByteArray(Charsets.UTF_8)
    val salt = "AUTH_V3_SALT".toByteArray(Charsets.UTF_8)
    val info = "device_report_signature".toByteArray(Charsets.UTF_8)
    val keyLength = 32
    
    val hkdf = HKDFBytesGenerator(SHA256Digest())
    hkdf.init(HKDFParameters(ikm, salt, info))
    val derivedKey = ByteArray(keyLength)
    hkdf.generateBytes(derivedKey, 0, keyLength)
    
    // 2.2 计算文件 SHA256
    fun sha256Hex(content: ByteArray): String {
        val digest = MessageDigest.getInstance("SHA-256")
        val hashBytes = digest.digest(content)
        return hashBytes.joinToString("") { "%02x".format(it) }
    }
    
    val assetsSha256 = sha256Hex(assetsContent)
    val vulnerabilitiesSha256 = sha256Hex(vulnerabilitiesContent)
    val weakPasswordsSha256 = sha256Hex(weakPasswordsContent)
    val reportHtmlSha256 = sha256Hex(reportHtmlContent)
    
    // 2.3 组装签名数据(严格顺序)
    val signPayload = buildString {
        append(taskId)
        append(inspectionId)
        append(assetsSha256)
        append(vulnerabilitiesSha256)
        append(weakPasswordsSha256)
        append(reportHtmlSha256)
    }
    
    // 2.4 计算 HMAC-SHA256
    val mac = Mac.getInstance("HmacSHA256")
    val secretKey = SecretKeySpec(derivedKey, "HmacSHA256")
    mac.init(secretKey)
    val signatureBytes = mac.doFinal(signPayload.toByteArray(Charsets.UTF_8))
    val deviceSignature = Base64.getEncoder().encodeToString(signatureBytes)
    
    // 2.5 生成 summary.json
    val summary = mapOf(
        "orgId" to 1173040813421105152L,
        "checkId" to inspectionId,
        "taskId" to taskId,
        "licence" to licence,
        "fingerprint" to fingerprint,
        "deviceSignature" to deviceSignature,
        "summary" to "检查摘要信息"
    )
    val summaryContent = objectMapper.writeValueAsString(summary).toByteArray(Charsets.UTF_8)
    
    // ========== 3. 生成 OpenPGP 签名 ==========
    
    // 3.1 生成 manifest.json
    val filesHashes = mapOf(
        "summary.json" to sha256Hex(summaryContent),
        "assets.json" to assetsSha256,
        "vulnerabilities.json" to vulnerabilitiesSha256,
        "weakPasswords.json" to weakPasswordsSha256,
        "漏洞评估报告.html" to reportHtmlSha256
    )
    val manifest = mapOf("files" to filesHashes)
    val manifestContent = objectMapper.writeValueAsString(manifest).toByteArray(Charsets.UTF_8)
    
    // 3.2 生成 OpenPGP 签名(使用 BouncyCastle
    val signatureAsc = generatePGPSignature(manifestContent, privateKey, publicKey)
    
    // ========== 4. 打包 ZIP 文件 ==========
    ZipOutputStream(FileOutputStream(outputPath)).use { zipOut ->
        zipOut.putNextEntry(ZipEntry("summary.json"))
        zipOut.write(summaryContent)
        zipOut.closeEntry()
        
        zipOut.putNextEntry(ZipEntry("assets.json"))
        zipOut.write(assetsContent)
        zipOut.closeEntry()
        
        zipOut.putNextEntry(ZipEntry("vulnerabilities.json"))
        zipOut.write(vulnerabilitiesContent)
        zipOut.closeEntry()
        
        zipOut.putNextEntry(ZipEntry("weakPasswords.json"))
        zipOut.write(weakPasswordsContent)
        zipOut.closeEntry()
        
        zipOut.putNextEntry(ZipEntry("漏洞评估报告.html"))
        zipOut.write(reportHtmlContent)
        zipOut.closeEntry()
        
        zipOut.putNextEntry(ZipEntry("META-INF/manifest.json"))
        zipOut.write(manifestContent)
        zipOut.closeEntry()
        
        zipOut.putNextEntry(ZipEntry("META-INF/signature.asc"))
        zipOut.write(signatureAsc)
        zipOut.closeEntry()
    }
    
    println("报告 ZIP 文件生成成功: $outputPath")
}

五、平台端验证流程

平台端会按以下顺序验证:

  1. OpenPGP 签名验证(防篡改)

    • 读取 META-INF/manifest.jsonMETA-INF/signature.asc
    • 使用平台公钥验证签名
    • 验证所有文件的 SHA256 是否与 manifest.json 中的哈希值匹配
  2. 设备签名验证(授权)

    • summary.json 提取 licencefingerprinttaskIddeviceSignature
    • 验证 licence + fingerprint 是否已绑定
    • 验证 taskId 是否存在且属于该设备
    • 使用相同的 HKDF 派生密钥
    • 重新计算签名并与 deviceSignature 比较

六、常见错误和注意事项

6.1 设备签名验证失败

可能原因

  1. 密钥派生错误:确保使用正确的 saltinfo 参数
  2. 签名数据顺序错误:必须严格按照 taskId + inspectionId + SHA256(...) 的顺序
  3. SHA256 格式错误:必须是 hex 字符串(小写),不能包含分隔符
  4. 文件内容错误:确保使用原始文件内容,不能进行编码转换
  5. licence 或 fingerprint 不匹配:确保与平台绑定的值一致

6.2 OpenPGP 签名验证失败

可能原因

  1. 私钥不匹配:确保使用与平台公钥对应的私钥
  2. manifest.json 格式错误:确保 JSON 格式正确
  3. 文件哈希值错误:确保 manifest.json 中的哈希值与实际文件匹配

6.3 文件缺失

必需文件

  • summary.json(必须包含授权字段)
  • assets.json
  • vulnerabilities.json
  • weakPasswords.json(文件名大小写不敏感)
  • 漏洞评估报告.html(文件名包含"漏洞评估报告"且以".html"结尾)
  • META-INF/manifest.json
  • META-INF/signature.asc

七、安全设计说明

7.1 为什么第三方无法伪造

  1. 设备签名

    • 只有拥有正确 licence + fingerprint 的设备才能派生正确的签名密钥
    • 即使第三方获取了某个设备的签名,也无法用于其他任务(taskId 绑定)
    • 即使第三方修改了报告内容,签名也会失效(多个文件的 SHA256 绑定)
  2. OpenPGP 签名

    • 只有拥有私钥的工具箱才能生成有效签名
    • 任何文件修改都会导致哈希值不匹配

7.2 密钥分离

使用 HKDF 的 info 参数区分不同用途的密钥:

  • device_report_signature:用于设备签名
  • 其他用途可以使用不同的 info 值,确保密钥隔离

八、测试建议

  1. 单元测试

    • 测试密钥派生是否正确
    • 测试签名生成和验证是否匹配
    • 测试文件 SHA256 计算是否正确
  2. 集成测试

    • 使用真实数据生成 ZIP 文件
    • 上传到平台验证是否通过
    • 测试篡改文件后验证是否失败
  3. 边界测试

    • 测试文件缺失的情况
    • 测试签名数据顺序错误的情况
    • 测试错误的 licencefingerprint 的情况

九、参考实现

  • HKDF 实现BouncyCastleJava/KotlinhkdfPython
  • HMAC-SHA256Java javax.crypto.Mac、Python hmac
  • OpenPGPBouncyCastleJava/KotlingnupgPython

十、联系支持

如有问题,请联系平台技术支持团队。