dsh-plugin-seal
已验证dsh-plugin-seal · v0.1.0 · MIT · Web 界面
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.
安装
dsh plugin add dsh-plugin-seal 用 dsh --profile default --dump-config 确认 layer 已生效 —— 参见安装指南。
源码
发布到 npm 但没有公开仓库。安装前请检查包内容。
标签
说明文档
seal
English · 中文
给 PDF 盖章并签名:合同章(指定页面盖章)、骑缝章(一枚章按页数等分盖在每页边缘,抽页 换页会在拼起来时留下缺口)、以及 PAdES 数字签名(用你的证书对整个文件做密码学签名)。
[!WARNING] 盖章和签名是两件事,别混。 盖上去的章是一张图片:不绑定身份,也不能证明此后文件没被 改动——拿到文件的人可以把它原样贴到别的文档上。《电子签名法》意义上的可靠电子签名要的是 证书绑定签署人 + 对文件字节的密码学签名,那是
seal_sign干的事。两者的分工是清楚的:章是给人看的,签名是给验证器看的。插件把这段写进系统提示词,模型在 调用前就知道,不会把盖章说成签署。
印章图片和证书都是你自己的。这些工具不会为任何组织绘制印章或签发证书。
五个工具
盖章和签名是两件事:盖章是给人看的,签名是给验证器看的。要让文件在争议中站得住,两件都做, 而且顺序不能反——先盖章,最后签名。
盖章不需要证书:seal_stamp 和 seal_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-right、bottom-left、bottom-center、top-right、top-left、center)配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。