使用 grunt-crypto 对构建产物进行私钥签名

解读

在国内前端工程化面试中,面试官抛出“用 grunt-crypto 给构建产物做私钥签名”这一题,表面看是考察 Grunt 插件的使用,实质想验证候选人对“构建安全”与“合规发布”的理解深度
由于国内企业对“软件供应链安全”越来越敏感(等保 2.0、关基、信创要求),构建产物一旦流出,必须能自证来源、防篡改、可追溯。因此,候选人需要把“跑通任务”升级为“落地一套可审计的签名方案”,并兼顾国密合规、密钥托管、CI 集成、性能损耗四大落地痛点。

知识点

  1. grunt-crypto 插件定位:基于 Node crypto 封装,仅提供基础加解密/签名/哈希任务,不解决密钥生命周期管理。
  2. 签名算法选择:
    • 国际场景:RSA-SHA256、ECDSA P-256
    • 国密合规场景:SM2-with-SM3(需自行扩展 grunt-crypto,调用 gm-crypto 或 node-gm)
  3. 密钥安全三原则:不落地明文、不随代码仓、不暴露在 CI 日志。国内主流做法:
    • 本地构建:通过 USBKey、国密卡或 Windows CNG 调用私钥
    • 云端构建:使用阿里云 KMS、腾讯云 KMS、华为云 KMS,通过 STS 临时凭证调用 Sign API
  4. 签名粒度:
    • 单文件级:对每个 .js/.css 生成 .sig,适合增量发布
    • 整包级:对 zip/tar.gz 做整体签名,减少 90% 签名次数,降低 KMS 调用费用
  5. 验签链路:
    • 运行时验签:在 CDN 边缘函数或 Nginx lua 层校验,失败直接 403,阻断投毒
    • 合规归档:把签名值、算法、证书序列号、时间戳写入 build-manifest.json供等保审计抽查
  6. Grunt 任务编排:
    • 先执行 grunt-contrib-uglifygrunt-webpack 等产生产物
    • 再串行 grunt-cryptosign 任务,必须设置 failOnError: true,一旦签名失败立即中断发布
    • 最后把 *.sig 与产物一起上传到 OSS,通过 grunt-oss-upload 插件开启服务端加密(OSS-KMS),形成“双保险”

答案

下面给出一条可直接落地到国内企业的最小可用路径,兼顾国际算法与国密算法两套方案,全部参数均通过环境变量注入,满足“密钥不落地”合规要求

  1. 安装依赖
npm i -D grunt-crypto gm-crypto  # gm-crypto 用于国密
  1. Gruntfile.js 中封装签名任务
module.exports = function(grunt) {
  grunt.initConfig({
    // 1. 国际算法示例:RSA-SHA256
    crypto: {
      signRSA: {
        options: {
          algorithm: 'RSA-SHA256',
          privateKey: () => {
            // 从 KMS 拉取私钥,或读取 USBKey
            return process.env.RSA_PRIVATE_KEY_PEM; // 仅内存
          },
          createSignatureFile: true, // 生成 .sig
          failOnError: true
        },
        files: [{
          expand: true,
          cwd: 'dist/',
          src: ['**/*.js', '**/*.css'],
          dest: 'dist/'
        }]
      },
      // 2. 国密算法示例:SM2-with-SM3
      signSM2: {
        options: {
          algorithm: 'sm2', // 扩展 grunt-crypto,内部调用 gm-crypto
          privateKey: () => process.env.SM2_PRIVATE_KEY_HEX,
          createSignatureFile: true,
          failOnError: true
        },
        files: [{
          expand: true,
          cwd: 'dist/',
          src: ['**/*.js'],
          dest: 'dist/'
        }]
      }
    }
  });

  grunt.loadNpmTasks('grunt-crypto');
  grunt.registerTask('dist', ['clean', 'uglify', 'crypto:signRSA', 'crypto:signSM2']);
};
  1. 在 GitLab CI(国内版)中注入密钥
variables:
  RSA_PRIVATE_KEY_PEM: "$KMS_RSA_KEY"  # 通过 KMS 接口获取
  SM2_PRIVATE_KEY_HEX: "$KMS_SM2_KEY"
script:
  - npm run dist
artifacts:
  paths:
    - dist/
  expire_in: 3 days
  1. 发布到 OSS 后,在运维侧预置公钥,并在 CDN 边缘函数中执行验签脚本,失败即回源阻断,实现“零信任”发布。

拓展思考

  1. 混合云场景:若公司部分业务部署在阿里云、部分在华为云,建议把签名任务拆成独立 Job,统一由“签名中台”完成,避免多 KMS 适配成本。
  2. 大产物优化:当构建包 > 500 MB 时,使用 grunt-tar 先打包再整体签名,可将 KMS 调用次数从千级降到个位,节省 80% 费用;同时利用分片上传 OSS 的 x-oss-meta-signature 头部存签名值,实现“边传边签”
  3. 双证书轮换:为满足等保对“密钥一年一换”的要求,可在 Gruntfile 中读取 CERT_VERSION 环境变量,动态选择证书版本,旧证书保留 180 天供历史版本回滚验签。
  4. 前端运行时验签:对微前端子应用,可在入口脚本中嵌入验签逻辑,使用 SubtleCrypto 校验 .sig,失败立即 location.href='about:blank'防止恶意子应用加载
  5. 合规审计:把 build-manifest.json 与签名文件一并存入不可篡改的日志仓(如阿里云 SLS 或腾讯 CLS),保留 5 年,满足等保 2.0 对“发布可追溯”条款的审查要求