简介这份iOS PDF电子签章资源面向需要快速集成电子签章功能的iOS开发人员专为合同、协议、保险单据等PDF文档提供原生加载与签署能力在设计上控制体积占用避免引入重量级WebView适合对包体积和运行效率有要求的App同时保留原生交互体验。压缩包一共包含7个文件其中有4个静态库.a文件、2个头文件.h和1个实现文件.mm整包大小55.69MB引入后无需额外源码即可使用。目前已有639人学习/下载该库的体积优势在大型App中尤为突出使用上不依赖额外框架创建控制器并推入导航栈即可完成PDF展示与签章入口便于嵌入既有流程。资源内提供的头文件与实现代码相互对照可快速梳理接口调用关系并理解签章层的内部处理逻辑若需要定制签章样式或增加附加功能可在此基础之上直接修改大大缩短从调研到上线的周期。适合已有iOS基础、希望在较短时间内落地电子签章能力的开发者和团队。 做iOS端PDF电子签章这个需求我前前后后折腾了大半个月。你要说它难其实SDK里几个接口就能把一张印章图片贴到PDF上但你要说它简单等到业务方、法务、审计拿着签完章的文件去别的平台验证时各种问题就全冒出来了。今天这篇就把我在iOS PDF电子签章这条路上踩过的坑、验证过的方案以及最终落地的一套实现思路完整拆开讲。如果你是做移动办公、电子合同、审批流相关开发的iOS工程师这篇文章应该能帮你少走不少弯路。先交代一下背景项目需要让用户在iPhone、iPad上打开一份PDF然后像纸质盖章一样在指定位置盖上公司印章或者手写签名签完的文件带法律效力能在桌面端、Web端、第三方法务系统里被验签。这也就意味着不能只做“贴图”而是要做真正的数字签名把签名信息和PDF内容做强绑定。1. 需求背景与技术选型思路1.1 为什么移动端签章不能照搬PC方案很多团队最开始会参考PC端的方案。PC上有非常成熟的企业级组件比如金格、iSignature这类浏览器里通过ActiveX或者本地服务就能完成证书读取、摘要计算、签名报文封装等一系列操作直接接管了PDF底层结构。但到了iOS上情况完全变了没有ActiveX这种底层通道App跑在沙盒里访问钥匙串受限连导出证书都有一堆权限判断。这就导致一个很现实的问题你不能把PC端那套“控件内嵌浏览器”的思路搬过来。iOS上能选的路径基本就三条只做可视化盖图、集成第三方商用SDK、自研生成签名域写回PDF。只做盖图最省事用PDFKit把印章图片绘制到PDFPage上再导出就行但没有密码学签名换个工具打开图片就没了也验不出签名者身份业务上基本过不了关。集成商用SDK比如PSPDFKit、PDFTron、Foxit签名功能确实开箱即用合规和兼容性也都帮你处理好了但价格不便宜而且对体量小的项目来说为一个章的功能引入一整个SDK包体积和预算都不太值。自研路线听起来硬核核心是把PDF的签名结构吃透其实就是构造符合PDF规范的签名字典。这条路前期投入最大但后期最灵活不依赖外部SDK想加批量签、骑缝章都可以自己控制。1.2 三条技术路线怎么选用一个表格来对比一下这三条路线是我当时自己做的选型笔记方案开发成本合规强度灵活性适合场景纯可视化盖图低无高内部预览、非正式场合商用SDK中高中预算充足、上线时间紧自研写签名域高高高有合规需求、团队能啃PDF底层我最终选的是“自研签名域系统Security框架”的组合。原因有两个一是项目有明确的验签要求必须产出标准PAdESPDF Advanced Electronic Signature兼容的签名二是团队后续还要做Android端希望两端底层逻辑对齐不绑定特定厂商SDK。如果你评估下来也想走自研这篇的核心实现部分可以直接参考。如果预算够商用SDK其实也没什么不好只是你需要额外调研一下对方在iOS上的验签能力覆盖得全不全。2. 签章原理与前置准备2.1 一个签章里到底装了什么很多人以为电子签章就是把一张章的照片贴到PDF上这是最大的误区。一个合规的PDF电子签名里面至少包含三个东西签名者的数字证书用于证明身份、签名值对PDF内容的摘要做加密运算后的结果、以及签名时的文档状态时间戳、签名域坐标等。这段公钥密码学逻辑是整个签章安全性的地基先对PDF文档内容计算摘要比如SHA-256再用签名者的私钥对摘要加密形成PKCS#7/CMS签名报文最后把这份报文嵌入PDF的签名域。验证的人用公钥解密签名值重新计算文档摘要两个摘要一致就说明文档在签名之后没有被改过。这里有一个非常关键的细节签名必须覆盖文档中除了签名值本身以外的全部内容。所以在计算摘要时要拿到PDF文件中签名域所在的位置把签名值预留出来只对它前面和后面的内容做摘要。这也是为什么自己写容易出问题——稍有先后的顺序错位签出来的文件在别人那里验签就会失败。2.2 iOS端证书与密钥体系的准备在iOS上做数字签名第一步是先拿到可用的私钥和证书。常见来源是.p12文件里面包含证书链和私钥。把.p12导入App后用Security framework的SecPKCS12Import方法可以解析出身份信息然后从中取出SecIdentityRef再取对应的SecKeyRef作为签名私钥。如果你没有现成的企业证书做测试可以用OpenSSL自己生成一套自签名测试证书。命令大概是openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes openssl pkcs12 -export -out cert.p12 -inkey key.pem -in cert.pem注意iOS真机调试时导入.p12会有几个坑。第一模拟器对钥匙串的很多行为跟真机不一样签名相关代码务必在真机上跑。第二SecPKCS12Import需要传一个密码字典密码为空时容易踩坑建议生成测试证书时都设上密码。第三如果你的App开启了“开发者模式”在iOS 16之后首次安装调试包时会有额外确认弹窗这个不能直接代码绕过只能在设备上手动确认。对后端对接也提一句如果你之前用过金格这类Java体系组件应该见过org.kg.bouncycastle.jce.provider.BouncyCastleProvider这种依赖Bouncy Castle在Java端是生成和解析PKCS#7报文的主力库。iOS端没有BC但Security.framework和openssl库能覆盖绝大多数场景。两端对接时核心是统一签名报文格式和证书链格式别在一端生成完、另一端解析不了。2.3 签章区域定位与PDF坐标系要在PDF指定位置上盖一个章你得先搞懂PDF的坐标系。PDF的坐标系原点在页面左下角单位是点point而UIKit的坐标系原点在左上角。如果你直接用UIView上拿到的点去PDF上找位置出来的结果经常会上下颠倒。以PDFKit为例当你使用PDFPage的时候可以通过boundsForBox:拿到页面大小然后做一次坐标映射func pdfPoint(from viewPoint: CGPoint, in page: PDFPage) - CGPoint { let bounds page.bounds(for: .mediaBox) return CGPoint(x: viewPoint.x, y: bounds.height - viewPoint.y) }这里还要考虑页面旋转的问题。PDFPage.rotationAngle如果不为0度坐标转换时要额外绕中心点旋转。常见的是90度、270度横过来的页面如果你忽略这一点章就可能盖到别的位置上。签章区域的尺寸也要根据PDF页面的实际宽度做等比缩放。我们项目里的做法是在PDF页面上先叠加一层可拖拽缩放的签章预览视图用户确认位置后再把预览视图的frame换算回PDF坐标系生成签名域/Annots的矩形坐标。这个方法交互自然而且能满足业务上“章必须盖在指定区域”的要求。3. 核心代码与实操过程3.1 读取PDF并展示签章页面第一步是把PDF文件加载进来。如果你已经有现成的PDF缓存到沙盒直接读取Data如果是从网络下载的建议先下载到本地临时目录再读取避免大文件占用太多内存。代码如下guard let document PDFDocument(url: fileURL) else { return } let page document.page(at: pageIndex) pdfView.document document pdfView.go(to: page) pdfView.autoScales true这里有个性能细节如果一个PDF有几百页一次性加载所有页面会非常吃内存。PDFKit底层有延迟渲染机制实际不会把所有页面绘制成位图但我们在叠加签章图层时尽量不要把所有页面都创建设置签章的容器视图按需创建就行。3.2 采集手写签名与生成印章位图业务上常见的签名来源有两种手写绘制和图片印章。手写采集用UIGraphicsImageRenderer加UIBezierPath就能实现关键是要把笔迹压感和速度反映出来这里不展开。印章图片如果是透明背景PNG可以直接用但要注意DPI。PDF里印章的大小是按点计算的一般推荐做成200~300像素宽的高清图不然打印出来糊成一片。生成签名位图的时候还要在图片四周预留一点边距因为某些验签软件会把签名图标缩小显示在文档角落预留边距能避免图标边缘贴边。从底层代码上看你最终需要的是一张只包含“印章视觉元素”的位图它只是给用户看的视觉反馈真正具备法律效力的是后面生成的签名值。3.3 计算文档摘要并生成PKCS#7签名数据这是整个实现中最核心的一步。在iOS上实现摘要计算和签名的方式是用SecKeyCreateSignature这个方法。它的输入是待签名的摘要数据输出是签名值。但这里的关键是到底待签名的是什么。正确做法是先定位到PDF文件中的签名域找到/ByteRange数组指示的位置。/ByteRange的格式是[start1 length1 start2 length2]签名值内容那一整块被挖空剩下的所有字节依次拼接得到摘要的数据源我们对这个数据源做SHA-256摘要再将摘要拿去做签名。具体到代码实现你需要先构造好签名值占位再把整个PDF解析出来。一个比较通用的流程是读取PDF文件二进制数据。找到签名域字典/Type /Sig所在偏移位置通过搜索/ByteRange关键字来定位。解析出当前/Contents的值通常是一个十六进制字符串或者流对象。计算/ByteRange中确定的挖空区域拼出待签名数据。调用SecKeyCreateSignature生成签名值。把签名值回填到原来/Contents的位置。示例代码片段如下注意签名算法要和证书匹配一般用.rsaSignatureMessagePKCS1v15SHA256private func signData(_ dataToSign: Data, privateKey: SecKey) - Data? { var error: UnmanagedCFError? guard let signature SecKeyCreateSignature( privateKey, .rsaSignatureMessagePKCS1v15SHA256, dataToSign as CFData, error ) as Data? else { if let e error?.takeRetainedValue() { print(签名失败: \(e)) } return nil } return signature }至于PKCS#7封装iOS原生Security.framework没有直接提供生成CMS结构的API我们项目里是用OpenSSL库比如openssl源码编译成静态库或者用libcrypto.a来做的。OpenSSL提供PKCS7_sign类似的接口可以把签名数据、证书链、附加属性等封装成完整的CMS报文。如果你不想碰C接口也可以用Swift封装好的SwiftOpenSSL库。封装完成后再把PKCS#7数据转成DER编码的Data最后替换掉PDF的/Contents字段。3.4 将签名写回PDF增量保存姿势写回方式分两种全量重写和增量保存。全量重写会把整个PDF重排原有对象的字节偏移全部变化但这样会破坏之前已经存在的签名域是不可行的。所以合规的PDF数字签名一定要用增量保存把新生成的签名对象追加到PDF文件尾部旧文件内容完全不动。如果你使用的是PDFKit它本身没有直接暴露增量保存的能力所以这里推荐自己控制文件写入流程读取原文件二进制把签名域中的/Contents占位替换成真实签名值再把整份PDF的数据追加写到原文件末尾。不要用PDFDocument.write(to:)那个方法会做全量重写签名之后你的验签必然失败。增量保存写完后还要更新/ByteRange里签名值的长度字段。这个字段在签名前就已经占好了坑真实签名值的长度不能超过占位长度否则文件结构就坏了。一般做法是预留4096字节签名数据如果比较长就提前把证书链压缩到合适的长度。3.5 签名后的自校验签完之后别急着交差先用第三方工具验一遍。常见做法是把文件丢到Adobe Acrobat Reader里看签名面板或者用pdftk、pdfsig这类命令行工具做验签。pdfsig是poppler库自带的命令用起来很方便pdfsig -verbose output_signed.pdf它会输出签名者、摘要算法、签名时间、文档是否被篡改等信息。如果这里提示“文档已被修改”或者“签名者身份未知”大概率是/ByteRange算错了或者证书链没带上。4. 常见问题与避坑技巧4.1 真机调试里的几个磨合点先说说调试环境的坑。真机连接Xcode调试时iOS 16以后需要开启开发者模式否则安装完App无法启动。这个不是代码问题但很耽误时间我在首次跑签名流程时就卡了一个下午一直以为是签名代码崩溃结果发现是设备设置里没开开发者模式。另外钥匙串访问权限在调试和发布包之间不一样。如果你在开发时用SecPKCS12Import导入过证书切到release包后有可能遇到“证书找不到”的情况因为钥匙串访问权限受entitlements控制。建议在调试阶段就单独配置一个keychain access group这样可以避免发布后钥匙串读取不到的尴尬。4.2 签完章的文件拿到其他端验证失败这个问题我调试了最久。现象是在自己iOS App里用SecTrustEvaluateWithError验证签名值是好的但放到Adobe里验证就提示签名无效。最终定位到两个原因第一是PKCS#7里没有带上完整的证书链。Adobe这类验签方需要完整的证书链来构建信任路径如果只塞了叶子证书中间证书缺失它就认为签名者不可信。解决方法是把证书链里所有证书都加到CMS的certificates集里。第二是时间戳问题。没有给签名附加可信时间戳时验签软件会把签名时间设置为本地时间而如果本地时间跟证书有效期对不上就会出现“签名时间不在证书有效期内”的告警。解决方法是接入一个RFC 3161时间戳服务把时间戳响应一并封装进PKCS#7。如果你不想因为时间戳再维护一个服务也可以先让签名时间取当前时间但要在交付说明里写清楚这个限制。4.3 中文字体在印章上显示乱码印章图片如果是位图不会有字体问题但如果你直接在PDF上绘制文字就一定要注意PDFKit默认字体对中文的支持并不好很容易出现乱码或者方框。最靠谱的做法是把印章上所有中文内容都渲染成位图再贴到PDF页面里不要直接往PDF里塞文字对象。如果需要动态生成带公司名称、日期、编号的印章建议用CoreGraphics把文字绘制到透明背景的CGContext上生成UIImage最后放到PDF页面上。这样既灵活又稳定不受PDF内嵌字体限制。4.4 大PDF文件的内存与性能优化一个几十MB的PDF在手机上处理内存很容易飙到几百MB甚至崩溃。我们接到的最大文件是一份两百多页的标书光解析PDF二进制数据就占了上百MB内存。优化方向有三个一是读取PDF二进制时用Data(contentsOf:options: .mappedIfSafe)内存映射文件而不是一次性全部读进内存二是签名前只做“定位签名域ByteRange替换追加写入”这几个操作全程不要调用任何渲染相关的API三是把整个签章流程放到后台线程主线程只负责进度条更新。最后实测下来200MB的PDF签名过程内存峰值能控制在200MB以内。5. 后续扩展与个人体会5.1 从单个签章到批量签章、骑缝章基础签章跑通之后业务方立刻就会提新需求批量签、骑缝章。批量签其实就是循环执行“定位下一个签名域→计算摘要→签名→回填”但要注意签名域的定位不能基于前一次签名后的新文件因为增量保存会不断追加内容偏移量会变。我们最终的做法是在原始PDF上把所有待签名的区域提前算好签名时用原始文件二进制一次性生成多份签名数据最后再做一次合并写入。这个方案兼容性非常好。骑缝章比较特殊它需要把印章图片分割成多段分别盖在相邻页面的边缘。位置和旋转角度都要根据每一页的实际内容动态计算如果PDF页面尺寸不同还需要按页宽等比缩放。目前iOS端实现骑缝章没有特别通用的库自己写的时候需要注意每页的出血位和边距否则装订后章印对不齐。5.2 一些经验之谈这套方案从调研到落地最深的体会是PDF签章本质上不是UI功能而是文件格式和密码学功能的结合体前期别急着写界面先把PDF规范和CMS报文结构研究透后面反而最快。还有一个经验是一定要给自己的签名实现留下一个“开关”当换用第三方SDK时核心的签名值生成和验签部分可以保持接口不变只替换底层实现。这样未来如果团队想切换到商业SDK不会伤筋动骨。最后提醒一句只要涉及真正走业务的电子合同测试时一定要用自签名测试证书把“撤销、过期、被篡改”这三类场景都跑一遍确认App能给出正确的失败提示。这个小细节在合规审计时会被重点关注很多人都是在审计前才发现自己的App只会弹“签名成功”。把这一条做好比任何花哨的功能都更让你省心。本文还有配套的精品资源点击获取