Skip to content

dsh-plugin-seal

Verified

dsh-plugin-seal · v0.1.0 · MIT · Web UI

Stamp a company seal onto a PDF — a contract seal on chosen pages, and a straddle seal split across every page edge so a swapped or removed page shows. Renders a seal image; it is not a cryptographic signature.

Install

dsh plugin add dsh-plugin-seal

Confirm the layer applied with dsh --profile default --dump-config — see the install guide.

Source

Published to npm without a public repository. Inspect the package contents before installing.

Tags

Readme

seal

English · 中文

给 PDF 盖章并签名:合同章(指定页面盖章)、骑缝章(一枚章按页数等分盖在每页边缘,抽页 换页会在拼起来时留下缺口)、以及 PAdES 数字签名(用你的证书对整个文件做密码学签名)。

[!WARNING] 盖章和签名是两件事,别混。 盖上去的章是一张图片:不绑定身份,也不能证明此后文件没被 改动——拿到文件的人可以把它原样贴到别的文档上。《电子签名法》意义上的可靠电子签名要的是 证书绑定签署人 + 对文件字节的密码学签名,那是 seal_sign 干的事。

两者的分工是清楚的:章是给人看的,签名是给验证器看的。插件把这段写进系统提示词,模型在 调用前就知道,不会把盖章说成签署。

印章图片和证书都是你自己的。这些工具不会为任何组织绘制印章或签发证书。

五个工具

盖章和签名是两件事:盖章是给人看的,签名是给验证器看的。要让文件在争议中站得住,两件都做, 而且顺序不能反——先盖章,最后签名

盖章不需要证书:seal_stampseal_straddle 只要一张印章图,证书只有 seal_sign 用。 只想盖个章、不做数字签名,完全可以。

seal_to_pdf — 先把 Word 转成 PDF

seal_to_pdf(input_path="合同.docx")   → 合同.pdf(5 页)

章盖在 PDF 上,而合同多半是 .docx。转换用 LibreOffice(需自行安装,或在设置里给 sofficePath),是明示的一步而不是顺手做掉的:

[!IMPORTANT] 转换会替换字体、可能改变分页,而转换之后,那份 PDF 才是"文件"——它才是被盖章、被签名、 出事时被拿出来的那一份。盖章前先看一眼成品,原始 .docx 保留不动。

直接拿 .docx 去盖章会被拒绝,并指向这个工具。

seal_stamp — 合同章

seal_stamp(pdf_path, pages="last", anchor="bottom-right", width_mm=40, rotation=-8)
  • 默认自己找位置(anchor: "auto"):读出文档里每个词的坐标,找到「甲方(盖章)」「签章」 这类字样并盖在那里。固定位置在真实合同上必然出错——实测一份 5 页协议,签署块在第 4 页, 盖"最后一页右下角"会落在空白处。多方合同用 party: "甲方" 指定盖谁那一栏;指定了就绝不会 盖到对方那一栏上。找不到就退回右下角,并在结果里说明"没找到,已退回"。定位到的位置可以用 offset_x_mm / offset_y_mm 微调(默认右移 6mm):章通常要压住标签后面的公司名,而不是标签 本身——实测这份协议用 offset_x_mm: 22 正好压在【Linkspire Limited】上。
  • 背景自动抠掉:扫描/拍照的印章是不透明的,直接盖等于用白卡片盖住条款。底色是从图片边缘 认出来的——白纸底、绿幕底、灰底都行,印章是什么颜色都行。已带透明通道的图片原样使用。
  • 页面:"3""1,4""2-5""all""first""last"(给了就不再自动找)。同一页 写两遍只盖一次——印章是半透明的,盖两次看得出来。
  • 位置:命名锚点(bottom-rightbottom-leftbottom-centertop-righttop-leftcenter)配 margin_mm,或者直接给 x_mm / y_mm(从左下角量,和 PDF 坐标系一致)。
  • 尺寸:只给直径,另一边按图片比例算——圆章不会被压扁成椭圆(压扁的章一眼就假)。
  • 越界不自动纠正:章跑到页面外面会如实报告是哪几条边,而不是悄悄挪回来。挪回来 意味着章盖在了签署人没选的位置。
  • 页面不是 A4 会被点名:章看起来不对,最常见的原因是页面尺寸和你以为的不一样——40mm 的章 在 A4 上正好,在 437mm 宽的页面上就是个小点,而坐标本身看不出任何问题。
  • 旋转:绕锚点旋转,所以报告的坐标是旋转前的锚点,角度大时看一眼成品。

seal_straddle — 骑缝章

seal_straddle(pdf_path, edge="right", width_mm=40, pages_per_seal=20)

一枚章按组内页数等分,每页在边缘留一条。页数多要分组:60 页盖一枚章,每页只剩不到 1 毫米的一线,既看不出也证明不了什么——线下做法同样是分批盖,pages_per_seal 就是那个批量。 组尾只剩一页时会并入上一组:一页的"骑缝"就是整枚章,什么都证明不了。

实现上,每页画的是完整的章,但用裁剪窗口只露出属于这一页的那一条。不这么做而是把章画到 页面外面靠边界"裁掉",内容其实仍然留在文件里,能被提取出来——那不叫裁掉。

seal_cert — 免费签发一张自签证书

seal_cert(common_name="上海示例科技有限公司", output_path="公司.p12", passphrase="…")

没有证书也能先跑起来:本地生成一对密钥和一张自签证书,写出 .p12(私钥,0600 权限)和 .cer(公钥,给对方)。不联网、不花钱。

它的限度要说清楚:自签证书让签名在密码学上有效,但本身不证明你是谁。要让对方的阅读器 认这个签名,得把 .cer 给对方、对方核对指纹后选择信任——这适合"双方本来就认识"的场景(公司 内部审批、长期合作的两家公司互换一次证书)。要让陌生人或法庭直接采信,还是得用受信任 CA 签发 的证书。

各种"免费"的实际情况:

路子 免费 能证明什么 结论
seal_cert 自签 持有该密钥的人签的;身份靠双方线下认可 内部/熟人场景可用
免费 S/MIME(Actalis 等) 经验证的邮箱身份 部分阅读器认,不在 Adobe AATL
Let's Encrypt 不能用于文档签名(那是 TLS 服务器证书,EKU 是 serverAuth) ✗ 常见误解
国内电子签平台免费额度 部分 第三方 CA 存证,《电子签名法》口径最稳 签名在他们服务器上完成,不走本插件

[!IMPORTANT] .p12 里有私钥,谁拿到谁就能以你的名义签署。不要发给别人,不要进代码仓库。要给对方的是 .cer

seal_sign — PAdES 数字签名

seal_sign(pdf_path, p12_path, passphrase, reason="同意并签署本合同")

用你的 PKCS#12(.p12 / .pfx)证书对整个文件做 CMS 签名。这才是有法律分量的那一步: 此后任何改动——包括再盖一个章——都会被验签工具发现,而证书说明是谁签的。

在客户端里配置密钥(推荐)

Web 客户端的 设置 → 签章证书 有一页表单:填 .p12 路径和口令,保存即可。之后 seal_sign 只给 pdf_path 就行。

它和写在配置文件里的区别是口令的去处:这里保存的凭证进的是本机存储域,不写进 profile 的 cordis.patch.yml——那个文件会被备份、同步、误提交、贴进 issue,而存储域不会跟着它走。页面 永远不会把口令读回来,只告诉你"已保存";一个会返回口令的接口,等于把它塞进每一个打开过这页 的浏览器缓存和 devtools 日志。

[!NOTE] 存储域不是密钥串:文件在本机是未加密的,任何以你的身份运行的程序都读得到。它买到的是"口令不再 跟着配置文件到处走",不是"口令被保护起来了"。真正要更强的保护,用操作系统密钥串托管的证书。

也可以配置一次在插件设置里,之后调用只给 pdf_path:

- id: seal
  config:
    p12Path: '/Users/you/keys/公司.p12'
    passphraseEnv: 'SEAL_P12_PASSPHRASE'   # 推荐:口令放环境变量
    # passphrase: '…'                      # 也可以直接写,但见下面的警告

口令的取用顺序是:调用参数 → 客户端保存的凭证 → passphraseEnv 指定的环境变量 → 配置里的 passphrase。 配了 passphraseEnv 但那个变量没设,会直接报错而不是悄悄退回明文那个——否则你以为在用 A 口令,实际用的是 B。结果里会写明这次用的是哪个来源(只写来源,不写值)。

[!WARNING] 写在 passphrase 里的口令会以明文存进 profile 的 cordis.patch.yml,而且插件设置界面 不会打码(客户端表单目前不认 secret 标记,我核实过)。那个文件会被备份、同步、贴进 issue;谁同时拿到它和 .p12,谁就能以你的名义签署。所以启动时会有一条日志提醒, 而 passphraseEnv 不会有——它把口令留在配置文件之外。

  • 必须最后做。 签名覆盖的是签的那一刻存在的字节;对已签名的文件再盖章,等于追加了签名没 覆盖的内容,每个阅读器都会报"文档已被修改"。所以对已签名的文件盖章会被直接拒绝,并提示 回到未签名的原件重来。
  • 只能签一次。 第二次签名会重写整个文件,把第一个签名弄失效——多方会签需要能做增量更新 (incremental update)的工具,这个插件不做,而不是假装做了。
  • 签名的分量取决于证书。 结果里会明确写出证书主体、签发者、是否自签、有效期。自签证书 产生的是"某个持有该密钥的人签的",在密码学上有效,但不证明他是谁;要对方认,需要受信任 CA 签发的证书。

验证不用信我们:pdfsig(poppler 自带)或 Adobe Reader 都能独立验签。

安装

dsh plugin --profile web add dsh-plugin-seal

然后在插件设置里把 sealPath 指向你的印章 PNG。签名用的证书每次调用时传路径。

典型流程

seal_stamp(合同.pdf, pages="last", anchor="bottom-right")   → 合同.sealed.pdf
seal_straddle(合同.sealed.pdf, edge="right")                → 合同.sealed.sealed.pdf
seal_sign(…, p12_path=公司证书.p12, passphrase=…)            → …signed.pdf

配置

默认值 决定什么
sealPath (空) 默认印章图片;留空则每次调用都要显式传 seal_path
widthMm 40 印章直径(国内公章常见 40mm)
opacity 0.9 不透明度;真实印泥会透出下面的字
marginMm 20 命名锚点距页边的距离
maxPagesPerSeal 20 一枚骑缝章最多跨多少页
removeBackground true 盖章前自动抠掉印章图片的背景
backgroundTolerance 40 背景色判定容差
sofficePath (空) LibreOffice 路径,留空则自动查找
p12Path (空) 默认签名证书路径;这里只是路径,私钥仍在那个文件里
passphraseEnv (空) 存放口令的环境变量名——推荐
passphrase (空) 口令本身,会明文存进 profile 配置
overwrite false 是否允许写回原文件

默认不覆盖原件:盖章不可逆,而未盖章的原件正是出事时用来比对的东西。默认输出 <原名>.sealed.pdf

它会拒绝的事

  • 加密的 PDF:pdf-lib 能打开,但写回去时那层保护就没了——"盖章成功"会变成"顺手把密码去掉了"。 所以直接拒绝,并说明要先自行解除保护。
  • JPEG 印章:JPEG 没有透明通道,盖上去是一个白底红圈,会把下面的条款挡住。仍然可以用, 但结果里会明确警告。
  • 单页文档盖骑缝章:一页无缝可骑。
  • 对已签名的文件盖章或再签:那会让已有签名失效。

依赖

本包用 pdf-lib(MIT)做 PDF 操作, @signpdf(MIT)+ node-forge (BSD-3)做 PAdES 签名与证书。PKCS#12 signer 是我们自己写的,没用 @signpdf/signer-p12: 它在证书名含非 ASCII 字符时会产出所有验证器都判无效的签名——而那正是"中国公司用自己的名字签署" 这个场景。详见修掉的一个上游 bug。这是本仓库唯一带运行时依赖的插件——手写 PDF 写入器或 CMS 签名实现来避免它们,等于用没人验证过的代码去处理别人要签字的文件。

修掉的一个上游 bug

@signpdf/signer-p12 会把 node-forge 解析出来的证书重新编码进签名里。而对含非 ASCII 的 DN,这个"解析→重编码"不是幂等的:forge 把 UTF8String 当作原始字节返回,再编码时又按 UTF-8 编了一遍。实测一张 CN=上海示例科技有限公司 的真实证书,79 字节的 issuer 变成了 121 字节, 于是签名里指名的签发者和任何证书都对不上——pdfsig 显示签署人为空、Signature is Invalid。 也就是说:每一家用自己中文名签署的公司,拿到的都是一份"看起来签了、验起来是坏的"合同。

所以本包自带 signer,除了在 forge 重编码之前把这些值解码回来以外,和上游实现完全一致。并配了 回归测试:中文名证书签出来的文件,pdfsig 必须判有效,且名字要正确显示。

许可证

MIT——见 LICENSE