You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 08:01:23