Node.js 使用 OpenSSL 以数字方式签署和验证日志文件

Node.js signing and verifying log files digitally using OpenSSL

根据当地的数据保留法,在我国运营的公司有义务每天以数字方式存储和签署日志文件,以便以后可以使用密钥验证文件完整性(以确保文件不存在) t 在签署后被篡改)以防法院命令。

我使用 Morgan 保存每日服务器日志,它每天使用 YYYYMMDD 格式创建日志,如下所示:access-20151123.log

我按照下面的教程创建了密钥。

请注意,我现在正在 Mac OSX 上对此进行测试。

https://github.com/coolaj86/node-ssl-root-cas/wiki/Painless%20Self-Signed%20Certificates%20in%20node.js

现在我在三个不同的文件夹下有 7 个文件。

全部

客户

服务器

我打算使用 node-rsa 包来签名(带有时间戳)并验证我的日志文件。我有点困惑我是否可以只使用 node-rsa 包(没有上面的教程)来完成所有工作。

现在,我如何签署位于 /logs 文件夹中的特定日志文件并在签署后测试其完整性(验证)?那是我的问题。 (例如,如果我编辑签名的日志文件并使用密钥对其进行测试以验证其完整性,它应该会抛出错误)

我不是专门寻找 "step by step" 指南,因此欢迎任何类型的建设性贡献。

编辑#2

以下部分是用土耳其语编写的,我在使用 PHP 时遵循了这些教程。现在我停止使用 PHP 并移动到 Node 堆栈,所以我想用 Node.

处理同样的事情

1-) https://www.syslogs.org/openssl-ile-5651-sayili-kanun-geregi-log-imzalamak/

2-) https://www.syslogs.org/openssl-1-0-x-tsa-ozelligi-ve-5651a-uygun-log-imzalamak/

这是我的 bash 脚本,但我会用 Node.js

来处理这部分

3-) https://www.syslogs.org/openssl-ve-tsa-ile-otomatik-log-imzalayici-shell-script/

每天向 运行 添加一个 cronjob,以使用 openssl 查找和签署日志文件:

find /logs -name \*.log -mtime +1 -exec openssl dgst -sha1 -sign my-server.key.pem -out {}.sig {} \;

您可以像这样验证签名:

openssl dgst -sha1 -verify my-server.crt.pem -signature access-2015-11-25.log.sig access-2015-11-25.log

重要编辑

最初的答案更笼统 "how to sign data"。这个问题已经变得更加明确,需要时间戳授权签署。修改后的答案如下,下面是原始答案。

需要 OpenSSL >= 0.9.9

时间戳 (ts) 在 0.9.9 之前不是一个可用的 OpenSSL 命令。 Mac 用户将需要使用 Homebrew 或类似软件下载合适的版本,并且可能需要明确指定每次调用的路径。

结构和证书

我们需要先生成要在整个过程中使用的证书。这涉及轻微的配置修改、创建基本目录结构以及对 openssl.

的几次调用

出于本演示的目的,我们假定基本目录为 ~/.openssl。如果该目录不存在则创建该目录

cd ~/.openssl
export TSA_DIR=$(pwd)
mkdir ca 
mkdir ca/private 
mkdir ca/newcerts 
touch ca/index.txt 
echo "0000000000000001" > ca/serial 
echo "0000000000000001" > ca/tsaserial 
export OPENSSL_CONF=$PWD/openssl.cnf 

将配置文件从您当前的 openssl 目录复制到此目录。就我而言,我在 /usr/local/etc/openssl/openssl.cnf

找到了它
cp /usr/local/etc/openssl/openssl.cnf .

您需要稍微修改此文件以取消注释以 extendedKeyUsage 开头的行。您可以手动执行此操作,或使用 sed

sed -i -e 's/\# extendedKeyUsage/extendedKeyUsage/' openssl.cnf

接下来,我们将设置密钥和证书。我们需要 CA 密钥CA 证书TSA 密钥 TSA 证书.

openssl genrsa 4096 > ca/private/cakey.pem
openssl req -new -x509 -days 3650 -key ca/private/cakey.pem > ca/newcerts/cacert.pem

随心所欲地回答后面的问题,但请务必记住,您的回答必须与两个证书一致。之后,我们将制作证书的副本。

cp ca/newcerts/cacert.pem ca

现在我们将生成 TSA 密钥TSA 证书签名请求

openssl genrsa 4096 > ca/private/tsakey.pem
openssl req -new -key ca/private/tsakey.pem > tsacert.csr

又一轮提问。答案必须一致。之后,我们将 CSR 转换为证书(您必须对两个问题回答 Y)

openssl ca -in tsacert.csr > ca/newcerts/tsacert.pem
cp ca/newcerts/tsacert.pem ca

投入使用

那么,节点在哪里?我们可以包装命令来创建 ts queryreply 文件,另一个来验证整个过程。让我们开始吧。

以下脚本假定它是我们上面导出的 $TSA_DIR 中的 运行。如果不是,请调整定义以适应。

'use strict';


const ssl = '/usr/local/bin/openssl';

const path = require('path');
const exec = require('child_process').exec;


const key = path.resolve(__dirname, 'ca/private/tsakey.pem');
const cert = path.resolve(__dirname, 'ca/tsacert.pem');
const ca = path.resolve(__dirname, 'ca/cacert.pem');
const config = path.resolve(__dirname, 'openssl.cnf');



function generateQuery(logfile, callback) {
  const dirname = path.dirname(logfile);
  const basename = path.basename(logfile, path.extname(logfile));
  const query = path.resolve(dirname, `${basename}.tsq`);

  const cmd = `${ssl} ts -query -data ${logfile} -policy tsa_policy1 > ${query}`;
  const child = exec(cmd, (err, stdout, stderr) => {
    if (err) return callback(err);

    // no stdout

    const cmd = `${ssl} ts -query -in ${query} -text`;
    const child2 = exec(cmd, (err, stdout, stderr) => {
      if (err) return callback(err);

      // successful stdout:
      // Hash Algorithm: sha1
      // Message data:
      //     0000 - 3d 2b 54 8f 5f da 76 08-09 d8 0f b4 e3 98 7e 87   =+T._.v.......~.
      //     0010 - a6 03 b4 bd                                       ....
      // Policy OID: tsa_policy1
      // Nonce: 0x0B3B80EDD01053D5
      // Certificate required: no
      // Extensions:



      callback(null, query);
    })
  })

}


function generateReply(query, callback) {
  const dirname = path.dirname(query);
  const basename = path.basename(query, path.extname(query));
  const reply = path.resolve(dirname, `${basename}.tsr`);

  const cmd = `${ssl} ts -config ${config} -reply -queryfile ${query} -inkey ${key} -signer ${cert} > ${reply}`;
  const child = exec(cmd, (err, stdout, stderr) => {
    if (err) return callback(err);

    const cmd = `${ssl} ts -reply -in ${reply} -text`;
    const child2 = exec(cmd, (err, stdout, stderr) => {
      if (err) return callback(err);

      // Successful stdout:
      // Status info:
      // Status: Granted.
      // Status description: unspecified
      // Failure info: unspecified
      //
      // TST info:
      // Version: 1
      // Policy OID: tsa_policy1
      // Hash Algorithm: sha1
      // Message data:
      //     0000 - 3d 2b 54 8f 5f da 76 08-09 d8 0f b4 e3 98 7e 87   =+T._.v.......~.
      //     0010 - a6 03 b4 bd                                       ....
      // Serial number: 0x05
      // Time stamp: Dec  1 18:02:07 2015 GMT
      // Accuracy: 0x01 seconds, 0x01F4 millis, 0x64 micros
      // Ordering: yes
      // Nonce: 0x95DFDBA4DCE33E7B
      // TSA: DirName:/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd/OU=section/CN=example.com/emailAddress=rw@xandocs.com
      // Extensions:

      callback(null, reply);
    })
  })

}



function validateToken(logfile, query, reply, callback) {
  const cmd = `${ssl} ts -verify -queryfile ${query} -in ${reply} -CAfile ${ca} -untrusted ${cert}`;
  const child = exec(cmd, (err, stdout, stderr) => {
    if (err) return callback(err);

    // Successful stdout:
    // Verification: OK

    const cmd = `${ssl} ts -verify -data ${logfile} -in ${reply} -CAfile ${ca} -untrusted ${cert}`;
    const child2 = exec(cmd, (err, stdout, stderr) => {
      if (err) return callback(err);

      // Successful stdout:
      // Verification: OK

      callback(null, stdout);
    })
  })

}



// sample use for all steps
const logfile = './foo.txt';

generateQuery(logfile, (err, query) => {
  if (err) console.log(err);

  generateReply(query, (err, reply) => {
    if (err) console.log(err);

    validateToken(logfile, query, reply, (err, result) => {
      if (err) console.log(err);

      console.log(result);
    })
  })
})

这就是您的 Node.js 应用程序中的 TSA 查询、回复和验证。

再编辑一次

我大量借用了this thread。应有尽有。


原答案如下

如果我对问题的理解正确,您有一系列日志文件,您必须对这些文件进行数字签名以验证每个文件的完整性。正确吗?

你提到了 node-rsa,所以我在这里坚持使用 RSA,你可能会在 运行 通读几次后发现它非常简单。我也避免使用任何模块,但在了解了这个过程的实际工作原理之后,如果您愿意的话,使用一个模块应该没有问题。我们也将从头开始研究按键。

当我们对文件进行数字签名时,我们实际上是在对文件内容应用加密哈希函数以生成摘要——本质上是一种单向数学归约到一个难以从原始内容猜测的可管理值,并且不可能从原始内容返回到原始内容。当然,还有其他属性。然后,我们使用 私钥 加密此摘要,可能连同日期等其他内容一起加密。结果是一个 签名,任何有权访问您的 public 密钥 的人都可以对其进行解密以进行验证。那么让我们开始吧。

生成密钥

生成私钥就像:

一样简单
openssl genrsa 4096

但是你会想用密码保护它并可能将它输出到文件中,所以只要添加几个标志就可以得到:

openssl genrsa -aes256 -out private_key.pem

现在我们从私钥生成一个public密钥如下

openssl rsa -in private_key.pem -outform PEM -pubout -out public_key.pem

就像魔术一样——呃,数学——你的密钥对可以使用了。保持 私钥 与任何其他机密信息一样安全,但 public 密钥 可以自由分发。让我们使用这些。

计算摘要

接下来,我们将使用 Node.js 核心 crypto 模块来计算摘要和符号。如果愿意,您也可以非常轻松地编写一个模块来生成密钥;奖金学分,我想。对于这个例子,我假设日志文件在脚本目录的一个子目录中;不太可能,但除非您需要帮助,否则我会将目录映射留给您。

在您的签名模块中,创建 crypto.Hash 的实例并将所需日志文件的内容读入该实例(使用 ES6 功能以获得额外信用):

'use strict';
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');

const hasher = crypto.createHash('sha256');

const pathname = path.resolve(__dirname, 'logs', 'access-20151201.log');
const rs = fs.createReadStream(pathname);

rs.on('data', data => hasher.update(data))

rs.on('end', () => {

  const hash = hasher.digest('hex');

  // we'll be back to this spot

}

这是摘要。现在我们需要使用您的 私钥 对其进行加密。

签署摘要

Node.js 使这部分变得非常简单。我们只需将 私钥 加载到内存中,然后使用 crypto 模块对其进行签名。让我们直接回到上面留下的评论:

const digest = hasher.digest('hex');

const privateKey = fs.readFileSync('private_key.pem');

const signer = crypto.createSign('RSA-SHA256');

signer.update(digest)

const signature = signer.sign(privateKey, 'base64')

就这么简单!

验证签名

您可以通过解密签名并将结果与​​文件摘要进行比较来验证任何签名。让我们运行通过其中之一。

const publicKey = fs.readFileSync('public_key.pem');

const verifier = crypto.createVerify('RSA-SHA256');

const testSignature = verifier.verify(publicKey, signature, 'base64');

const verified = testSignature === digest;

好了。我建议您尽可能多地阅读,了解它的确切工作原理。这将帮助您克服任何意想不到的困难,而且加密是数学的奇迹,本身就令人着迷。

祝您好运 - 希望对您有所帮助。很高兴听到我在这里是否有任何错误。