Node.js中如何获取PDF的DocTimeStamp?现有签名代码不兼容
获取PDF中的文档时间戳(DocTimeStamp)
服务器中的该PDF已添加时间戳,我需要获取其最后校验时间,即DocTimeStamp。当前代码可正常获取普通签名信息,但无法获取文档时间戳。
现有获取普通签名信息的代码:
let { PDFDocument, rgb, PDFDict, PDFName, PDFHexString, PDFString } = require('pdf-lib'); const extractSignatureInfo = async (pdfBuf) => { console.log('extractSignatureInfo'); // Load the signed PDF document const pdfDoc = await PDFDocument.load(pdfBuf); const signatureInfoList = []; // Iterate through all fields in the document const acroFormDict = pdfDoc.context.lookup(pdfDoc.catalog.get(PDFName.of('AcroForm'))); if (acroFormDict){ const fieldsArray = acroFormDict.get(PDFName.of('Fields')).asArray(); for (const fieldRef of fieldsArray) { const field = pdfDoc.context.lookup(fieldRef); // Use lookup to get the actual PDFRef const fieldType = field.get(PDFName.of('FT'))?.asString(); if (fieldType === '/Sig') { const signatureDictRef = field.get(PDFName.of('V')); const signatureDict = pdfDoc.context.lookup(signatureDictRef); // Use lookup for the signatureDict if (signatureDict instanceof PDFDict) { const signatureInfo = { fieldName: field.get(PDFName.of('T'))?.asString(), certificateName: signatureDict.get(PDFName.of('Name'))?.toString(), signingTime: signatureDict.get(PDFName.of('M'))?.toString(), }; let reason = signatureDict.get(PDFName.of('Reason')) if (reason instanceof PDFString) { reason = decodeUTF16BE(reason) || reason.asString('utf-8'); } signatureInfo.reason = reason; signatureInfoList.push(signatureInfo); } } } } return signatureInfoList; }
解决方案:修改代码支持获取DocTimeStamp
文档时间戳是一种特殊签名,通常满足两个特征:
- 签名字典的
SubFilter字段值为/ETSI.RFC3161(符合RFC3161标准) - 时间戳信息可能直接存于签名字典的
DocTimeStamp条目,或嵌套在PKCS#7签名内容中
修改后的完整代码
let { PDFDocument, PDFDict, PDFName, PDFString } = require('pdf-lib'); const extractSignatureAndTimestampInfo = async (pdfBuf) => { console.log('extractSignatureAndTimestampInfo'); const pdfDoc = await PDFDocument.load(pdfBuf); const signatureInfoList = []; const acroFormDict = pdfDoc.context.lookup(pdfDoc.catalog.get(PDFName.of('AcroForm'))); if (acroFormDict){ const fieldsArray = acroFormDict.get(PDFName.of('Fields')).asArray(); for (const fieldRef of fieldsArray) { const field = pdfDoc.context.lookup(fieldRef); const fieldType = field.get(PDFName.of('FT'))?.asString(); if (fieldType === '/Sig') { const signatureDictRef = field.get(PDFName.of('V')); const signatureDict = pdfDoc.context.lookup(signatureDictRef); if (signatureDict instanceof PDFDict) { const signatureInfo = { fieldName: field.get(PDFName.of('T'))?.asString(), certificateName: signatureDict.get(PDFName.of('Name'))?.toString(), signingTime: signatureDict.get(PDFName.of('M'))?.toString(), isTimestamp: false, timestampTime: null }; // 识别RFC3161标准的时间戳签名 const subFilter = signatureDict.get(PDFName.of('SubFilter'))?.asString(); if (subFilter === '/ETSI.RFC3161') { signatureInfo.isTimestamp = true; // 尝试直接读取DocTimeStamp中的时间 const docTimeStamp = signatureDict.get(PDFName.of('DocTimeStamp')); if (docTimeStamp instanceof PDFDict) { signatureInfo.timestampTime = docTimeStamp.get(PDFName.of('M'))?.toString(); } } let reason = signatureDict.get(PDFName.of('Reason')) if (reason instanceof PDFString) { reason = decodeUTF16BE(reason) || reason.asString('utf-8'); } signatureInfo.reason = reason; signatureInfoList.push(signatureInfo); } } } } // 获取最后一个文档时间戳 const timestampList = signatureInfoList.filter(item => item.isTimestamp); if (timestampList.length > 0) { const lastTimestamp = timestampList[timestampList.length - 1]; console.log('最后校验时间(DocTimeStamp):', lastTimestamp.timestampTime); } return signatureInfoList; } // 补充原代码缺失的UTF16BE解码函数 function decodeUTF16BE(pdfString) { try { const hexStr = pdfString.asHexString(); const bytes = new Uint8Array(hexStr.match(/.{1,2}/g).map(byte => parseInt(byte, 16))); return new TextDecoder('UTF-16BE').decode(bytes); } catch (e) { return null; } }
进阶:解析PKCS#7获取时间戳(如果直接读取失败)
若DocTimeStamp条目不存在,时间戳信息可能嵌在PKCS#7签名内容中,可借助pkijs库解析:
// 先安装依赖:npm install pkijs const { ContentInfo, SignedData } = require('pkijs'); // 在签名处理逻辑中添加以下代码(替换原时间戳读取部分) if (subFilter === '/ETSI.RFC3161') { signatureInfo.isTimestamp = true; const contents = signatureDict.get(PDFName.of('Contents'))?.asHexString(); if (contents) { const rawBytes = Buffer.from(contents, 'hex'); const cmsContent = new ContentInfo({ schema: rawBytes }); const signedData = new SignedData({ schema: cmsContent.content }); // 遍历签名属性提取时间戳 for (const attr of signedData.signerInfos[0].signedAttrs.attributes) { // 时间戳属性OID:1.2.840.113549.1.9.16.2.14 if (attr.type === '1.2.840.113549.1.9.16.2.14') { const tsToken = attr.values[0]; const tsSignedData = new SignedData({ schema: tsToken.content }); for (const tsAttr of tsSignedData.signerInfos[0].signedAttrs.attributes) { // 签名时间OID:1.2.840.113549.1.9.5 if (tsAttr.type === '1.2.840.113549.1.9.5') { signatureInfo.timestampTime = tsAttr.values[0].value.toISOString(); break; } } break; } } } }
内容的提问来源于stack exchange,提问作者Brian Hoag
相关产品推荐
相关产品推荐

