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

使用Azure证书保管库非导出私钥实现PDF数字签名遇异常

Azure密钥保管库非导出私钥证书实现PDF签名问题排查

问题描述

我在用Azure密钥保管库中带非导出私钥的证书实现PDF数字签名时遇到问题:生成的PDF仅显示签名控件但无签名内容,Adobe验证证书时提示错误。

核心代码

主签名函数

async signDocument(fileBuffer: Buffer, certName: string) {
  try {
    Logger.log(`Signing document with certificate ${certName}...`);
    const certificate = await this.azureService.getCertificate(certName);
    Logger.log('Certificate:', certificate.keyId);

    // Load the original PDF
    const pdfDocument = await PDFDocument.load(fileBuffer);

    // Save the original PDF to verify it's being loaded correctly
    const originalPdf = await pdfDocument.save();
    fs.writeFileSync('original_document.pdf', originalPdf);  // Save for debugging

    // Placeholder widget coordinates (make sure they are within the page size)
    const widget = [50, 50, 200, 100];

    // Add signature placeholder
    pdflibAddPlaceholder({
      pdfDoc: pdfDocument,
      reason: 'The user is declaring consent through JavaScript.',
      contactInfo: 'signpdf@example.com',
      name: 'John Doe',
      location: 'Free Text Str., Free World',
      widgetRect: widget,
    });

    // Save the modified PDF with the placeholder
    const pdfWithPlaceholder = await pdfDocument.save();
    const pdfDigest = createHash(Buffer.from(pdfWithPlaceholder))
    const signature = await this.azureService.signWithAzureVault(certificate.keyId, pdfDigest)
    const AzureSigner = new AzureCustomSigner();
    AzureSigner.setAzureSignature(signature);
    const signedPdf = await AzureSigner.sign(Buffer.from(pdfWithPlaceholder));
    fs.writeFileSync('pdf_with_placeholder.pdf', signedPdf);
    Logger.log('Document signed successfully!');
    return signedPdf;

  } catch (error) {
    Logger.error('Error signing document:', error.message);
    return error; // Return the error object
  }
}

Azure签名服务方法

async signWithAzureVault(keyId: string, digest: Buffer) {
  try {
    Logger.log(`Signing data using Azure Key Vault with keyId: ${keyId}`);
    const { keyName } = parseKeyId(keyId);
    const key = await this.keyClient.getKey(keyName);
    const cryptographyClient = new CryptographyClient(key, this.credential);
    const signatureResult = await cryptographyClient.sign('RS256', digest);
    return Buffer.from(signatureResult.result);
  } catch (error) {
    Logger.error('Error signing with Azure Vault:', error.message);
    throw error;
  }
}

自定义AzureSigner实现

import { Signer, SignPdfError } from '@signpdf/signpdf';

export class AzureCustomSigner extends Signer {
  private signatureFromAzure: Buffer | null = null;

  constructor() {
    super();
  }

  setAzureSignature(signature: Buffer): void {
    this.signatureFromAzure = signature;
  }

  async sign(pdfBuffer: Buffer, signingTime?: Date): Promise<Buffer> {
    // Step 1: Ensure the Azure signature is set
    if (!this.signatureFromAzure) {
      throw new SignPdfError('Azure signature must be set before signing.', SignPdfError.TYPE_INPUT);
    }

    const pdfBytes = this._getPdfBytes(pdfBuffer);

    const signedPdfBytes = this._applyAzureSignature(pdfBytes, this.signatureFromAzure);

    return Buffer.from(signedPdfBytes);
  }

  private _getPdfBytes(pdfBuffer: Buffer): Uint8Array {
    return new Uint8Array(pdfBuffer);
  }

  private _applyAzureSignature(pdfBytes: Uint8Array, signatureFromAzure: Buffer): Uint8Array {
    const signatureHex = signatureFromAzure.toString('hex');

    const signaturePlaceholder = this._findSignaturePlaceholder(pdfBytes);
    return this._replacePlaceholderWithSignature(pdfBytes, signaturePlaceholder, signatureHex);
  }

  private _findSignaturePlaceholder(pdfBytes: Uint8Array): { position: number; length: number } {
    return {
      position: 12345, // This should be the byte position of the signature placeholder in the PDF.
      length: 256, // The expected length of the signature.
    };
  }

  private _replacePlaceholderWithSignature(
    pdfBytes: Uint8Array,
    placeholder: { position: number; length: number },
    signatureHex: string
  ): Uint8Array {
    const signatureBytes = Buffer.from(signatureHex, 'hex');

    if (signatureBytes.length > placeholder.length) {
      throw new SignPdfError('The signature is too large for the placeholder.');
    }

    const updatedPdfBytes = new Uint8Array(pdfBytes.length);
    updatedPdfBytes.set(pdfBytes);

    for (let i = 0; i < signatureBytes.length; i++) {
      updatedPdfBytes[placeholder.position + i] = signatureBytes[i];
    }

    if (signatureBytes.length < placeholder.length) {
      for (let i = signatureBytes.length; i < placeholder.length; i++) {
        updatedPdfBytes[placeholder.position + i] = 0x00;
      }
    }

    return updatedPdfBytes;
  }
}

export default AzureCustomSigner;

问题分析

直接指出几个致命错误:

  • 摘要计算错误:直接对带占位符的整个PDF文件哈希,而PDF签名要求的是对签名域中指定的待签字节范围计算哈希,不是整个文件
  • 占位符定位错误:硬编码的position:12345和length:256完全不符合实际PDF中的签名占位符位置,导致签名注入到错误位置
  • 签名格式错误:Azure返回的是原始RSA签名(PKCS#1格式),但PDF签名需要的是PKCS#7/CMS格式的签名数据,需要包含证书链和签名属性
  • 缺少证书嵌入:没有将Azure证书保管库中的证书(及链)嵌入到PDF签名域中,Adobe无法验证证书有效性

修复方案

步骤1:修正签名摘要提取逻辑

使用@signpdf/signpdf提供的工具函数提取待签字节范围,计算正确的哈希:

import { getSignatureContent } from '@signpdf/signpdf';

// 在生成pdfWithPlaceholder后:
const { data, signatureByteRange } = getSignatureContent(pdfWithPlaceholder);
const sha256Digest = createHash('sha256').update(data).digest();

步骤2:Azure服务补充证书链获取方法

// 在azureService中添加获取证书链的方法
async getCertificateChain(certName: string) {
  const cert = await this.certificateClient.getCertificate(certName);
  return cert.certificate; // 返回DER格式的证书链
}

步骤3:实现正确的自定义Signer

生成PKCS#7格式签名并正确定位占位符:

import { Signer, SignPdfError, getSignatureContent } from '@signpdf/signpdf';
import * as pkijs from 'pkijs';

export class AzureCustomSigner extends Signer {
  private signature: Buffer | null = null;
  private certChain: Buffer | null = null;

  setAzureSignature(signature: Buffer): void {
    this.signature = signature;
  }

  setCertificateChain(certChain: Buffer): void {
    this.certChain = certChain;
  }

  async sign(pdfBuffer: Buffer, signingTime = new Date()): Promise<Buffer> {
    if (!this.signature || !this.certChain) {
      throw new SignPdfError('Signature and certificate chain must be set.', SignPdfError.TYPE_INPUT);
    }

    const { data, placeholder } = getSignatureContent(pdfBuffer);
    const sha256Digest = createHash('sha256').update(data).digest();

    // 构建PKCS#7/CMS签名
    const cmsSignedData = await this.buildCMSSignedData(sha256Digest, signingTime);
    
    // 转换为DER格式并编码为Base64
    const cmsDer = cmsSignedData.toSchema().toBER(false);
    const cmsBase64 = Buffer.from(cmsDer).toString('base64');

    // 替换占位符,用空格填充到占位符长度
    const paddedSignature = cmsBase64.padEnd(placeholder.length, ' ');
    return Buffer.from(pdfBuffer.toString().replace(placeholder, paddedSignature));
  }

  private async buildCMSSignedData(digest: Buffer, signingTime: Date): Promise<pkijs.SignedData> {
    const certSchema = pkijs.fromBER(this.certChain);
    const cert = new pkijs.Certificate({ schema: certSchema });
    
    const signedData = new pkijs.SignedData({
      version: 1,
      digestAlgorithms: [new pkijs.AlgorithmIdentifier({ algorithmId: '1.2.840.113549.1.1.11' })], // SHA-256
      encapContentInfo: new pkijs.EncapsulatedContentInfo({
        eContentType: '1.2.840.113549.1.7.1', // data
        eContent: new pkijs.OCTET_STRING({ valueHex: digest }),
      }),
      certificates: [cert],
      signerInfos: [
        new pkijs.SignerInfo({
          version: 1,
          sid: new pkijs.IssuerAndSerialNumber({
            issuer: cert.issuer,
            serialNumber: cert.serialNumber,
          }),
          digestAlgorithm: new pkijs.AlgorithmIdentifier({ algorithmId: '1.2.840.113549.1.1.11' }),
          signatureAlgorithm: new pkijs.AlgorithmIdentifier({ algorithmId: '1.2.840.113549.1.1.1' }), // RSA
          signature: new pkijs.OCTET_STRING({ valueHex: this.signature }),
          signedAttrs: [
            new pkijs.Attribute({
              type: '1.2.840.113549.1.9.3', // contentType
              values: ['1.2.840.113549.1.7.1'],
            }),
            new pkijs.Attribute({
              type: '1.2.840.113549.1.9.5', // signingTime
              values: [signingTime],
            }),
            new pkijs.Attribute({
              type: '1.2.840.113549.1.9.4', // messageDigest
              values: [digest],
            }),
          ],
        }),
      ],
    });

    return signedData;
  }
}

步骤4:更新主签名函数

async signDocument(fileBuffer: Buffer, certName: string) {
  try {
    Logger.log(`Signing document with certificate ${certName}...`);
    const certificate = await this.azureService.getCertificate(certName);
    const certChain = await this.azureService.getCertificateChain(certName);
    Logger.log('Certificate:', certificate.keyId);

    // Load the original PDF
    const pdfDocument = await PDFDocument.load(fileBuffer);

    // Add signature placeholder
    const widget = [50, 50, 200, 100];
    pdflibAddPlaceholder({
      pdfDoc: pdfDocument,
      reason: 'The user is declaring consent through JavaScript.',
      contactInfo: 'signpdf@example.com',
      name: 'John Doe',
      location: 'Free Text Str., Free World',
      widgetRect: widget,
    });

    // Save the modified PDF with the placeholder
    const pdfWithPlaceholder = await pdfDocument.save();
    
    // 提取待签内容并计算哈希
    const { data } = getSignatureContent(pdfWithPlaceholder);
    const sha256Digest = createHash('sha256').update(data).digest();
    
    // 调用Azure签名
    const signature = await this.azureService.signWithAzureVault(certificate.keyId, sha256Digest);
    
    // 初始化自定义Signer并签名
    const azureSigner = new AzureCustomSigner();
    azureSigner.setAzureSignature(signature);
    azureSigner.setCertificateChain(certChain);
    const signedPdf = await azureSigner.sign(pdfWithPlaceholder);
    
    fs.writeFileSync('signed_document.pdf', signedPdf);
    Logger.log('Document signed successfully!');
    return signedPdf;

  } catch (error) {
    Logger.error('Error signing document:', error.message);
    throw error;
  }
}

关键注意事项

  • 确保pdflibAddPlaceholder生成的占位符长度足够容纳PKCS#7签名(建议至少设置为8192字节)
  • Azure密钥保管库中的证书必须包含完整的链(根证书+中间证书)
  • 签名算法必须与证书的密钥算法匹配(RS256对应RSA密钥)

内容的提问来源于stack exchange,提问作者Arvydas Kezunas

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 01:25:54